Browse Source

Add a bunch of XML docs

pull/988/head
Still Hsu 7 years ago
parent
commit
956019fb51
No known key found for this signature in database GPG Key ID: 8601A145FDA95209
67 changed files with 1389 additions and 267 deletions
  1. +1
    -0
      docs/docfx.json
  2. +1
    -0
      src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs
  3. +1
    -1
      src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs
  4. +6
    -1
      src/Discord.Net.Commands/CommandContext.cs
  5. +31
    -2
      src/Discord.Net.Commands/CommandService.cs
  6. +22
    -5
      src/Discord.Net.Commands/CommandServiceConfig.cs
  7. +20
    -7
      src/Discord.Net.Commands/ModuleBase.cs
  8. +3
    -0
      src/Discord.Net.Commands/MultiMatchHandling.cs
  9. +5
    -0
      src/Discord.Net.Commands/RunMode.cs
  10. +2
    -5
      src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs
  11. +1
    -1
      src/Discord.Net.Core/Entities/Channels/IChannel.cs
  12. +2
    -1
      src/Discord.Net.Core/Entities/Channels/IDMChannel.cs
  13. +2
    -1
      src/Discord.Net.Core/Entities/Channels/IGroupChannel.cs
  14. +50
    -4
      src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs
  15. +65
    -0
      src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs
  16. +4
    -1
      src/Discord.Net.Core/Entities/Channels/IPrivateChannel.cs
  17. +31
    -4
      src/Discord.Net.Core/Entities/Channels/ITextChannel.cs
  18. +2
    -0
      src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs
  19. +3
    -5
      src/Discord.Net.Core/Entities/Emotes/Emoji.cs
  20. +17
    -7
      src/Discord.Net.Core/Entities/Emotes/Emote.cs
  21. +1
    -1
      src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs
  22. +3
    -4
      src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs
  23. +121
    -43
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  24. +2
    -1
      src/Discord.Net.Core/Entities/IDeletable.cs
  25. +4
    -1
      src/Discord.Net.Core/Entities/IMentionable.cs
  26. +24
    -3
      src/Discord.Net.Core/Entities/Image.cs
  27. +258
    -15
      src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs
  28. +1
    -1
      src/Discord.Net.Core/Entities/Messages/EmbedField.cs
  29. +3
    -0
      src/Discord.Net.Core/Entities/Roles/Color.cs
  30. +17
    -3
      src/Discord.Net.Core/Entities/Roles/IRole.cs
  31. +8
    -1
      src/Discord.Net.Core/Entities/Users/IGuildUser.cs
  32. +2
    -3
      src/Discord.Net.Core/Entities/Users/IUser.cs
  33. +1
    -1
      src/Discord.Net.Core/Entities/Users/IVoiceState.cs
  34. +1
    -0
      src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs
  35. +29
    -0
      src/Discord.Net.Core/IDiscordClient.cs
  36. +9
    -3
      src/Discord.Net.Core/Logging/LogManager.cs
  37. +1
    -1
      src/Discord.Net.Core/RequestOptions.cs
  38. +14
    -11
      src/Discord.Net.Core/Utils/MentionUtils.cs
  39. +9
    -7
      src/Discord.Net.Core/Utils/Preconditions.cs
  40. +31
    -5
      src/Discord.Net.Rest/BaseDiscordClient.cs
  41. +6
    -22
      src/Discord.Net.Rest/DiscordRestClient.cs
  42. +4
    -1
      src/Discord.Net.Rest/DiscordRestConfig.cs
  43. +1
    -1
      src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs
  44. +2
    -1
      src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs
  45. +2
    -1
      src/Discord.Net.Rest/Net/Converters/ImageConverter.cs
  46. +5
    -3
      src/Discord.Net.Rest/Net/Converters/PermissionTargetConverter.cs
  47. +2
    -2
      src/Discord.Net.Rest/Net/Queue/RequestQueue.cs
  48. +9
    -7
      src/Discord.Net.Rest/Utils/TypingNotifier.cs
  49. +7
    -7
      src/Discord.Net.WebSocket/Audio/AudioClient.cs
  50. +4
    -4
      src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs
  51. +14
    -11
      src/Discord.Net.WebSocket/BaseSocketClient.cs
  52. +23
    -6
      src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs
  53. +30
    -8
      src/Discord.Net.WebSocket/DiscordShardedClient.cs
  54. +4
    -4
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  55. +43
    -12
      src/Discord.Net.WebSocket/DiscordSocketConfig.cs
  56. +4
    -1
      src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs
  57. +44
    -4
      src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs
  58. +4
    -1
      src/Discord.Net.WebSocket/Entities/Channels/ISocketPrivateChannel.cs
  59. +3
    -3
      src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs
  60. +25
    -1
      src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs
  61. +38
    -3
      src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs
  62. +46
    -2
      src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs
  63. +25
    -1
      src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs
  64. +32
    -0
      src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs
  65. +12
    -1
      src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs
  66. +173
    -7
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  67. +19
    -4
      src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs

+ 1
- 0
docs/docfx.json View File

@@ -37,6 +37,7 @@
"default",
"_template/light-dark-theme"
],
"postProcessors": [ "ExtractSearchIndex" ],
"overwrite": "_overwrites/**/**.md",
"globalMetadata": {
"_appTitle": "Discord.Net Documentation",


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

@@ -19,6 +19,7 @@ namespace Discord.Commands

/// <inheritdoc/>
/// <param name="overridenTypeReader">The <see cref="TypeReader"/> to be used with the parameter. </param>
/// <exception cref="ArgumentException">The given <paramref name="overridenTypeReader"/> does not inherit from <see cref="TypeReader"/>.</exception>
public OverrideTypeReaderAttribute(Type overridenTypeReader)
{
if (!TypeReaderTypeInfo.IsAssignableFrom(overridenTypeReader.GetTypeInfo()))


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

@@ -13,7 +13,7 @@ namespace Discord.Commands
/// <remarks>
/// <see cref="Preconditions" /> of the same group require only one of the preconditions to pass in order to
/// be successful (A || B). Specifying <see cref="Group" /> = <see langword="null" /> or not at all will
/// require *all* preconditions to pass, just like normal (A && B).
/// require *all* preconditions to pass, just like normal (A &amp;&amp; B).
/// </remarks>
public string Group { get; set; } = null;



+ 6
- 1
src/Discord.Net.Commands/CommandContext.cs View File

@@ -16,7 +16,12 @@ namespace Discord.Commands

/// <summary> Indicates whether the channel that the command is executed in is a private channel. </summary>
public bool IsPrivate => Channel is IPrivateChannel;

/// <summary>
/// Initializes a new <see cref="CommandContext" /> class with the provided client and message.
/// </summary>
/// <param name="client">The underlying client.</param>
/// <param name="msg">The underlying message.</param>
public CommandContext(IDiscordClient client, IUserMessage msg)
{
Client = client;


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

@@ -13,11 +13,14 @@ namespace Discord.Commands
{
public class CommandService
{
/// <summary>
/// Occurs when a command-related information is received.
/// </summary>
public event Func<LogMessage, Task> Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } }
internal readonly AsyncEvent<Func<LogMessage, Task>> _logEvent = new AsyncEvent<Func<LogMessage, Task>>();

/// <summary>
/// Fired when a command is successfully executed without any runtime error.
/// Occurs when a command is successfully executed without any runtime error.
/// </summary>
public event Func<CommandInfo, ICommandContext, IResult, Task> CommandExecuted { add { _commandExecutedEvent.Add(value); } remove { _commandExecutedEvent.Remove(value); } }
internal readonly AsyncEvent<Func<CommandInfo, ICommandContext, IResult, Task>> _commandExecutedEvent = new AsyncEvent<Func<CommandInfo, ICommandContext, IResult, Task>>();
@@ -51,7 +54,16 @@ namespace Discord.Commands
/// </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);

/// <summary>
/// Initializes a new <see cref="CommandService"/> class.
/// </summary>
public CommandService() : this(new CommandServiceConfig()) { }

/// <summary>
/// Initializes a new <see cref="CommandService" /> class with the provided configuration.
/// </summary>
/// <param name="config">The configuration class.</param>
/// <exception cref="InvalidOperationException">The <see cref="RunMode"/> is set to <see cref="RunMode.Default"/>.</exception>
public CommandService(CommandServiceConfig config)
{
_caseSensitive = config.CaseSensitiveCommands;
@@ -121,6 +133,7 @@ namespace Discord.Commands
/// A built module.
/// </returns>
public Task<ModuleInfo> AddModuleAsync<T>(IServiceProvider services) => AddModuleAsync(typeof(T), services);

/// <summary>
/// Adds a command module from a <see cref="Type" /> .
/// </summary>
@@ -132,6 +145,8 @@ namespace Discord.Commands
/// <returns>
/// A built module.
/// </returns>
/// <exception cref="ArgumentException">This module has already been added.</exception>
/// <exception cref="InvalidOperationException">The <see cref="ModuleInfo"/> fails to be built; an invalid type may have been provided.</exception>
public async Task<ModuleInfo> AddModuleAsync(Type type, IServiceProvider services)
{
services = services ?? EmptyServiceProvider.Instance;
@@ -209,7 +224,7 @@ namespace Discord.Commands
/// </summary>
/// <param name="module">The <see cref="ModuleInfo" /> to be removed from the service.</param>
/// <returns>
/// Returns whether the <paramref name="module"/> is successfully removed.
/// Returns whether the module is successfully removed.
/// </returns>
public async Task<bool> RemoveModuleAsync(ModuleInfo module)
{
@@ -223,7 +238,21 @@ namespace Discord.Commands
_moduleLock.Release();
}
}
/// <summary>
/// Removes the command module.
/// </summary>
/// <typeparam name="T">The <see cref="Type"/> of the module.</typeparam>
/// <returns>
/// Returns whether the module is successfully removed.
/// </returns>
public Task<bool> RemoveModuleAsync<T>() => RemoveModuleAsync(typeof(T));
/// <summary>
/// Removes the command module.
/// </summary>
/// <param name="type">The <see cref="Type"/> of the module.</param>
/// <returns>
/// Returns whether the module is successfully removed.
/// </returns>
public async Task<bool> RemoveModuleAsync(Type type)
{
await _moduleLock.WaitAsync().ConfigureAwait(false);


+ 22
- 5
src/Discord.Net.Commands/CommandServiceConfig.cs View File

@@ -2,23 +2,40 @@ using System;

namespace Discord.Commands
{
/// <summary>
/// Represents a configuration class for <see cref="CommandService" />.
/// </summary>
public class CommandServiceConfig
{
/// <summary> Gets or sets the default RunMode commands should have, if one is not specified on the Command attribute or builder. </summary>
/// <summary>
/// Gets or sets the default <see cref="RunMode" /> commands should have, if one is not specified on the
/// Command attribute or builder.
/// </summary>
public RunMode DefaultRunMode { get; set; } = RunMode.Sync;

/// <summary>
/// Gets or sets the <see cref="char"/> that separates an argument with another.
/// </summary>
public char SeparatorChar { get; set; } = ' ';
/// <summary> Determines whether commands should be case-sensitive. </summary>
/// <summary>
/// Gets or sets whether commands should be case-sensitive.
/// </summary>
public bool CaseSensitiveCommands { get; set; } = false;

/// <summary> Gets or sets the minimum log level severity that will be sent to the Log event. </summary>
/// <summary>
/// Gets or sets the minimum log level severity that will be sent to the <see cref="CommandService.Log"/> event.
/// </summary>
public LogSeverity LogLevel { get; set; } = LogSeverity.Info;

/// <summary> Determines whether RunMode.Sync commands should push exceptions up to the caller. </summary>
/// <summary>
/// Gets or sets whether <see cref="RunMode.Sync"/> commands should push exceptions up to the caller.
/// </summary>
public bool ThrowOnError { get; set; } = true;

/// <summary> Determines whether extra parameters should be ignored. </summary>
/// <summary>
/// Gets or sets whether extra parameters should be ignored.
/// </summary>
public bool IgnoreExtraArgs { get; set; } = false;

///// <summary> Gets or sets the <see cref="IServiceProvider"/> to use. </summary>


+ 20
- 7
src/Discord.Net.Commands/ModuleBase.cs View File

@@ -9,32 +9,45 @@ namespace Discord.Commands
public abstract class ModuleBase<T> : IModuleBase
where T : class, ICommandContext
{
/// <summary>
/// The underlying context of the command.
/// </summary>
/// <seealso cref="T:Discord.Commands.ICommandContext" />
/// <seealso cref="T:Discord.Commands.CommandContext" />
public T Context { get; private set; }

/// <summary>
/// Sends a message to the source channel
/// Sends a message to the source channel.
/// </summary>
/// <param name="message">Contents of the message; optional only if <paramref name="embed"/> is specified</param>
/// <param name="isTTS">Specifies if Discord should read this message aloud using TTS</param>
/// <param name="embed">An embed to be displayed alongside the message</param>
/// <param name="message">
/// Contents of the message; optional only if <paramref name="embed" /> is specified.
/// </param>
/// <param name="isTTS">Specifies if Discord should read this <paramref name="message"/> aloud using text-to-speech.</param>
/// <param name="embed">An embed to be displayed alongside the <paramref name="message"/>.</param>
protected virtual async Task<IUserMessage> ReplyAsync(string message = null, bool isTTS = false, Embed embed = null, RequestOptions options = null)
{
return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false);
}
/// <summary>
/// The method to execute before executing the command.
/// The method to execute before executing the command.
/// </summary>
/// <param name="command">The <see cref="CommandInfo"/> of the command to be executed.</param>
protected virtual void BeforeExecute(CommandInfo command)
{
}
/// <summary>
/// The method to execute after executing the command.
/// The method to execute after executing the command.
/// </summary>
/// <param name="command"></param>
/// <param name="command">The <see cref="CommandInfo"/> of the command to be executed.</param>
protected virtual void AfterExecute(CommandInfo command)
{
}

/// <summary>
/// The method to execute when building the module.
/// </summary>
/// <param name="commandService">The <see cref="CommandService"/> used to create the module.</param>
/// <param name="builder">The builder used to build the module.</param>
protected virtual void OnModuleBuilding(CommandService commandService, ModuleBuilder builder)
{
}


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

@@ -1,5 +1,8 @@
namespace Discord.Commands
{
/// <summary>
/// Specifies the behavior when multiple matches are found during the command parsing stage.
/// </summary>
public enum MultiMatchHandling
{
/// <summary> Indicates that when multiple results are found, an exception should be thrown. </summary>


+ 5
- 0
src/Discord.Net.Commands/RunMode.cs View File

@@ -1,5 +1,10 @@
namespace Discord.Commands
{
/// <summary>
/// Specifies the behavior of the command execution workflow.
/// </summary>
/// <seealso cref="CommandServiceConfig"/>
/// <seealso cref="CommandAttribute"/>
public enum RunMode
{
/// <summary>


+ 2
- 5
src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs View File

@@ -20,16 +20,13 @@ namespace Discord
/// When modifying an <see cref="ITextChannel" />, the <see cref="Name" />
/// MUST be alphanumeric with dashes. It must match the following RegEx: [a-z0-9-_]{2,100}
/// </remarks>
/// <exception cref="Discord.Net.HttpException">
/// A BadRequest will be thrown if the name does not match the above RegEx.
/// </exception>
public Optional<string> Name { get; set; }
/// <summary>
/// Moves the channel to the following position. This is 0-based!
/// Moves the channel to the following position. This property is zero-based.
/// </summary>
public Optional<int> Position { get; set; }
/// <summary>
/// Gets or sets the category for this channel.
/// Gets or sets the category ID for this channel.
/// </summary>
public Optional<ulong?> CategoryId { get; set; }
}


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

@@ -4,7 +4,7 @@ using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// Represents a generic Discord channel.
/// Represents a generic channel.
/// </summary>
public interface IChannel : ISnowflakeEntity
{


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

@@ -3,7 +3,7 @@ using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// Represents a generic DM channel.
/// Represents a generic direct-message channel.
/// </summary>
public interface IDMChannel : IMessageChannel, IPrivateChannel
{
@@ -15,6 +15,7 @@ namespace Discord
/// <summary>
/// Closes this private channel, removing it from your channel list.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
Task CloseAsync(RequestOptions options = null);
}
}

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

@@ -3,13 +3,14 @@ using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// Represents a private generic group channel.
/// Represents a generic private group channel.
/// </summary>
public interface IGroupChannel : IMessageChannel, IPrivateChannel, IAudioChannel
{
/// <summary>
/// Leaves this group.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
Task LeaveAsync(RequestOptions options = null);
}
}

+ 50
- 4
src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs View File

@@ -5,34 +5,52 @@ using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// Represents a guild channel (text, voice, category).
/// Represents a generic guild channel.
/// </summary>
/// <seealso cref="ITextChannel"/>
/// <seealso cref="IVoiceChannel"/>
/// <seealso cref="ICategoryChannel"/>
public interface IGuildChannel : IChannel, IDeletable
{
/// <summary>
/// Gets the position of this channel in the guild's channel list, relative to others of the same type.
/// Gets the position of this channel.
/// </summary>
/// <returns>
/// The position of this channel in the guild's channel list, relative to others of the same type.
/// </returns>
int Position { get; }

/// <summary>
/// Gets the parent ID (category) of this channel in the guild's channel list.
/// </summary>
/// <returns>
/// The parent category ID associated with this channel, or <see langword="null"/> if none is set.
/// </returns>
ulong? CategoryId { get; }
/// <summary>
/// Gets the parent channel (category) of this channel.
/// </summary>
Task<ICategoryChannel> GetCategoryAsync();
/// <summary>
/// Gets the guild this channel is a member of.
/// Gets the guild associated with this channel.
/// </summary>
/// <returns>
/// The guild that this channel belongs to.
/// </returns>
IGuild Guild { get; }
/// <summary>
/// Gets the id of the guild this channel is a member of.
/// Gets the guild ID associated with this channel.
/// </summary>
/// <returns>
/// The guild ID that this channel belongs to.
/// </returns>
ulong GuildId { get; }
/// <summary>
/// Gets a collection of permission overwrites for this channel.
/// </summary>
/// <returns>
/// A collection of overwrites associated with this channel.
/// </returns>
IReadOnlyCollection<Overwrite> PermissionOverwrites { get; }

/// <summary>
@@ -50,49 +68,77 @@ namespace Discord
/// <param name="isUnique">
/// If <see langword="true"/>, don't try to reuse a similar invite (useful for creating many unique one time use invites).
/// </param>
/// <param name="options">
/// The options to be used when sending the request.
/// </param>
Task<IInviteMetadata> CreateInviteAsync(int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null);
/// <summary>
/// Returns a collection of all invites to this channel.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null);

/// <summary>
/// Modifies this guild channel.
/// </summary>
/// <param name="func">The properties to modify the channel with.</param>
/// <param name="options">The options to be used when sending the request.</param>
Task ModifyAsync(Action<GuildChannelProperties> func, RequestOptions options = null);

/// <summary>
/// Gets the permission overwrite for a specific role, or <see langword="null"/> if one does not exist.
/// </summary>
/// <param name="role">The role to get the overwrite from.</param>
OverwritePermissions? GetPermissionOverwrite(IRole role);
/// <summary>
/// Gets the permission overwrite for a specific user, or <see langword="null"/> if one does not exist.
/// </summary>
/// <param name="user">The user to get the overwrite from.</param>
OverwritePermissions? GetPermissionOverwrite(IUser user);
/// <summary>
/// Removes the permission overwrite for the given role, if one exists.
/// </summary>
/// <param name="role">The role to remove the overwrite from.</param>
/// <param name="options">The options to be used when sending the request.</param>
Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null);
/// <summary>
/// Removes the permission overwrite for the given user, if one exists.
/// </summary>
/// <param name="user">The user to remove the overwrite from.</param>
/// <param name="options">The options to be used when sending the request.</param>
Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null);

/// <summary>
/// Adds or updates the permission overwrite for the given role.
/// </summary>
/// <param name="role">The role to add the overwrite to.</param>
/// <param name="permissions">The overwrite to add to the role.</param>
/// <param name="options">The options to be used when sending the request.</param>
Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null);
/// <summary>
/// Adds or updates the permission overwrite for the given user.
/// </summary>
/// <param name="user">The user to add the overwrite to.</param>
/// <param name="permissions">The overwrite to add to the user.</param>
/// <param name="options">The options to be used when sending the request.</param>
Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null);

/// <summary>
/// Gets a collection of all users in this channel.
/// </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>
new IAsyncEnumerable<IReadOnlyCollection<IGuildUser>> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets a user in this channel with the provided ID.
/// </summary>
/// <param name="id">The ID of the user.</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>
new Task<IGuildUser> GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
}
}

+ 65
- 0
src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs View File

@@ -13,50 +13,115 @@ namespace Discord
/// <summary>
/// Sends a message to this message channel.
/// </summary>
/// <param name="text">The message to be sent.</param>
/// <param name="isTTS">Whether the message should be read aloud by Discord or not.</param>
/// <param name="embed">The <see cref="EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param>
Task<IUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null);
#if FILESYSTEM
/// <summary>
/// Sends a file to this message channel, with an optional caption.
/// </summary>
/// <param name="filePath">The file path of the file.</param>
/// <param name="text">The message to be sent.</param>
/// <param name="isTTS">Whether the message should be read aloud by Discord or not.</param>
/// <param name="embed">The <see cref="EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <remarks>
/// If you wish to upload an image and have it embedded in a <see cref="EmbedType.Rich"/> embed, you may
/// upload the file and refer to the file with "attachment://filename.ext" in the
/// <see cref="Discord.EmbedBuilder.ImageUrl"/>.
/// </remarks>
Task<IUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null);
#endif
/// <summary>
/// Sends a file to this message channel, with an optional caption.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> of the file to be sent.</param>
/// <param name="filename">The name of the attachment.</param>
/// <param name="text">The message to be sent.</param>
/// <param name="isTTS">Whether the message should be read aloud by Discord or not.</param>
/// <param name="embed">The <see cref="EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <remarks>
/// If you wish to upload an image and have it embedded in a <see cref="EmbedType.Rich"/> embed, you may
/// upload the file and refer to the file with "attachment://filename.ext" in the
/// <see cref="Discord.EmbedBuilder.ImageUrl"/>.
/// </remarks>
Task<IUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null);

/// <summary>
/// Gets a message from this message channel with the given id, or <see langword="null"/> if not found.
/// </summary>
/// <param name="id">The ID of the message.</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>
/// The message gotten from either the cache or the download, or <see langword="null"/> if none is found.
/// </returns>
Task<IMessage> GetMessageAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);

/// <summary>
/// Gets the last N messages from this message channel.
/// </summary>
/// <param name="limit">The numbers of message to be gotten from.</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>
/// Paged collection of messages. Flattening the paginated response into a collection of messages with
/// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> is required if you wish to access the messages.
/// </returns>
IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch,
CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets a collection of messages in this channel.
/// </summary>
/// <param name="fromMessageId">The ID of the starting message to get the messages from.</param>
/// <param name="dir">The direction of the messages to be gotten from.</param>
/// <param name="limit">The numbers of message to be gotten from.</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>
/// Paged collection of messages. Flattening the paginated response into a collection of messages with
/// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> is required if you wish to access the messages.
/// </returns>
IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch,
CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);

/// <summary>
/// Gets a collection of messages in this channel.
/// </summary>
/// <param name="fromMessage">The starting message to get the messages from.</param>
/// <param name="dir">The direction of the messages to be gotten from.</param>
/// <param name="limit">The numbers of message to be gotten from.</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>
/// Paged collection of messages. Flattening the paginated response into a collection of messages with
/// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> is required if you wish to access the messages.
/// </returns>
IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch,
CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets a collection of pinned messages in this channel.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A collection of messages.
/// </returns>
Task<IReadOnlyCollection<IMessage>> GetPinnedMessagesAsync(RequestOptions options = null);

/// <summary>
/// Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
Task TriggerTypingAsync(RequestOptions options = null);
/// <summary>
/// Continuously broadcasts the "user is typing" message to all users in this channel until the returned
/// object is disposed.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
IDisposable EnterTypingState(RequestOptions options = null);
}
}

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

@@ -8,8 +8,11 @@ namespace Discord
public interface IPrivateChannel : IChannel
{
/// <summary>
/// Users that can access this channel.
/// Gets the users that can access this channel.
/// </summary>
/// <returns>
/// A collection of users that can access this channel.
/// </returns>
IReadOnlyCollection<IUser> Recipients { get; }
}
}

+ 31
- 4
src/Discord.Net.Core/Entities/Channels/ITextChannel.cs View File

@@ -11,40 +11,67 @@ namespace Discord
public interface ITextChannel : IMessageChannel, IMentionable, IGuildChannel
{
/// <summary>
/// Gets whether the channel is NSFW.
/// Determines whether the channel is NSFW.
/// </summary>
/// <returns>
/// <see langword="true"/> if the channel has the NSFW flag enabled; otherwise, <see langword="false"/>.
/// </returns>
bool IsNsfw { get; }

/// <summary>
/// Gets the current topic for this text channel.
/// </summary>
/// <returns>
/// The topic set in the channel, or <see langword="null"/> if none is set.
/// </returns>
string Topic { get; }

/// <summary>
/// Bulk deletes multiple messages.
/// Bulk-deletes multiple messages.
/// </summary>
/// <param name="messages">The messages to be bulk-deleted.</param>
/// <param name="options">The options to be used when sending the request.</param>
Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null);
/// <summary>
/// Bulk deletes multiple messages.
/// Bulk-deletes multiple messages.
/// </summary>
/// <param name="messageIds">The IDs of the messages to be bulk-deleted.</param>
/// <param name="options">The options to be used when sending the request.</param>
Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null);

/// <summary>
/// Modifies this text channel.
/// </summary>
/// <param name="func">The properties to modify the channel with.</param>
/// <param name="options">The options to be used when sending the request.</param>
Task ModifyAsync(Action<TextChannelProperties> func, RequestOptions options = null);

/// <summary>
/// Creates a webhook in this text channel.
/// </summary>
/// <param name="name">The name of the webhook.</param>
/// <param name="avatar">The avatar of the webhook.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// The created webhook.
/// </returns>
Task<IWebhook> CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null);
/// <summary>
/// Gets the webhook in this text channel with the provided ID, or <see langword="null"/> if not found.
/// Gets the webhook in this text channel with the provided ID.
/// </summary>
/// <param name="id">The ID of the webhook.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A webhook associated with the <paramref name="id"/>, or <see langword="null"/> if not found.
/// </returns>
Task<IWebhook> GetWebhookAsync(ulong id, RequestOptions options = null);
/// <summary>
/// Gets the webhooks for this text channel.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A collection of webhooks.
/// </returns>
Task<IReadOnlyCollection<IWebhook>> GetWebhooksAsync(RequestOptions options = null);
}
}

+ 2
- 0
src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs View File

@@ -21,6 +21,8 @@ namespace Discord
/// <summary>
/// Modifies this voice channel.
/// </summary>
/// <param name="func">The properties to modify the channel with.</param>
/// <param name="options">The options to be used when sending the request.</param>
Task ModifyAsync(Action<VoiceChannelProperties> func, RequestOptions options = null);
}
}

+ 3
- 5
src/Discord.Net.Core/Entities/Emotes/Emoji.cs View File

@@ -6,10 +6,8 @@ namespace Discord
public class Emoji : IEmote
{
// TODO: need to constrain this to Unicode-only emojis somehow
/// <summary>
/// Gets the Unicode representation of this emote.
/// </summary>

/// <inheritdoc />
public string Name { get; }
/// <summary>
/// Gets the Unicode representation of this emote.
@@ -28,7 +26,7 @@ namespace Discord
/// <summary>
/// Determines whether the specified emoji is equal to the current emoji.
/// </summary>
/// <param name="obj">The object to compare with the current object.</param>
/// <param name="other">The object to compare with the current object.</param>
public override bool Equals(object other)
{
if (other == null) return false;


+ 17
- 7
src/Discord.Net.Core/Entities/Emotes/Emote.cs View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.Globalization;

namespace Discord
@@ -6,15 +7,12 @@ namespace Discord
/// <summary>
/// A custom image-based emote.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class Emote : IEmote, ISnowflakeEntity
{
/// <summary>
/// Gets the display name (tooltip) of this emote.
/// </summary>
/// <inheritdoc />
public string Name { get; }
/// <summary>
/// Gets the ID of this emote.
/// </summary>
/// <inheritdoc />
public ulong Id { get; }
/// <summary>
/// Gets whether this emote is animated.
@@ -36,6 +34,10 @@ namespace Discord
Animated = animated;
}

/// <summary>
/// Determines whether the specified emote is equal to the current emote.
/// </summary>
/// <param name="other">The object to compare with the current object.</param>
public override bool Equals(object other)
{
if (other == null) return false;
@@ -47,6 +49,7 @@ namespace Discord
return string.Equals(Name, otherEmote.Name) && Id == otherEmote.Id;
}

/// <inheritdoc />
public override int GetHashCode()
{
unchecked
@@ -57,7 +60,8 @@ namespace Discord

/// <summary> Parses an <see cref="Emote"/> from its raw format. </summary>
/// <param name="text">The raw encoding of an emote; for example, &lt;:dab:277855270321782784&gt;.</param>
/// <returns>An emote</returns>
/// <returns>An emote.</returns>
/// <exception cref="ArgumentException">Invalid emote format.</exception>
public static Emote Parse(string text)
{
if (TryParse(text, out Emote result))
@@ -65,6 +69,9 @@ namespace Discord
throw new ArgumentException("Invalid emote format.", nameof(text));
}

/// <summary> Tries to parse an <see cref="Emote"/> from its raw format. </summary>
/// <param name="text">The raw encoding of an emote; for example, &lt;:dab:277855270321782784&gt;.</param>
/// <param name="result">An emote.</param>
public static bool TryParse(string text, out Emote result)
{
result = null;
@@ -89,6 +96,9 @@ namespace Discord
}

private string DebuggerDisplay => $"{Name} ({Id})";
/// <summary>
/// Returns the raw representation of the emote.
/// </summary>
public override string ToString() => $"<{(Animated ? "a" : "")}:{Name}:{Id}>";
}
}

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

@@ -31,7 +31,7 @@ namespace Discord

private string DebuggerDisplay => $"{Name} ({Id})";
/// <summary>
/// Gets the raw representation of the emoji.
/// Gets the raw representation of the emote.
/// </summary>
public override string ToString() => $"<{(Animated ? "a" : "")}:{Name}:{Id}>";
}


+ 3
- 4
src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs View File

@@ -8,9 +8,8 @@ namespace Discord
/// await Context.Guild.ModifyAsync(async x =&gt;
/// {
/// x.Name = "aaaaaah";
/// x.RegionId = (await Context.Client.GetOptimalVoiceRegionAsync()).Id;
/// });
/// </code>
/// </code>
/// </example>
/// <see cref="T:Discord.IGuild" />
public class GuildProperties
@@ -61,11 +60,11 @@ namespace Discord
/// </summary>
public Optional<ulong?> AfkChannelId { get; set; }
/// <summary>
/// Gets or sets the <see cref="ITextChannel" /> where System messages should be sent.
/// Gets or sets the <see cref="ITextChannel" /> where system messages should be sent.
/// </summary>
public Optional<ITextChannel> SystemChannel { get; set; }
/// <summary>
/// Gets or sets the ID of the <see cref="ITextChannel" /> where System messages should be sent.
/// Gets or sets the ID of the <see cref="ITextChannel" /> where system messages should be sent.
/// </summary>
public Optional<ulong?> SystemChannelId { get; set; }
/// <summary>


+ 121
- 43
src/Discord.Net.Core/Entities/Guilds/IGuild.cs View File

@@ -20,8 +20,11 @@ namespace Discord
/// </summary>
int AFKTimeout { get; }
/// <summary>
/// Returns <see langword="true"/> if this guild is embeddable (e.g. widget).
/// 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.
/// </returns>
bool IsEmbeddable { get; }
/// <summary>
/// Gets the default message notifications for users who haven't explicitly set their notification settings.
@@ -37,29 +40,32 @@ namespace Discord
/// </summary>
VerificationLevel VerificationLevel { get; }
/// <summary>
/// Returns the ID of this guild's icon, or <see langword="null"/> if one is not set.
/// Returns the ID of this guild's icon, or <see langword="null"/> if none is set.
/// </summary>
string IconId { get; }
/// <summary>
/// Returns the URL of this guild's icon, or <see langword="null"/> if one is not set.
/// Returns the URL of this guild's icon, or <see langword="null"/> if none is set.
/// </summary>
string IconUrl { get; }
/// <summary>
/// Returns the ID of this guild's splash image, or <see langword="null"/> if one is not set.
/// Returns the ID of this guild's splash image, or <see langword="null"/> if none is set.
/// </summary>
string SplashId { get; }
/// <summary>
/// Returns the URL of this guild's splash image, or <see langword="null"/> if one is not set.
/// Returns the URL of this guild's splash image, or <see langword="null"/> if none is set.
/// </summary>
string SplashUrl { get; }
/// <summary>
/// Determines if this guild is currently connected and ready to be used.
/// </summary>
/// <returns>
/// Returns <see langword="true"/> if this guild is currently connected and ready to be used. Only applies
/// to the WebSocket client.
/// </summary>
/// </returns>
bool Available { get; }

/// <summary>
/// Gets the ID of the AFK voice channel for this guild if set, or <see langword="null"/> if not.
/// Gets the ID of the AFK voice channel for this guild, or <see langword="null"/> if none is set.
/// </summary>
ulong? AFKChannelId { get; }
/// <summary>
@@ -67,11 +73,11 @@ namespace Discord
/// </summary>
ulong DefaultChannelId { get; }
/// <summary>
/// Gets the ID of the embed channel for this guild if set, or <see langword="null"/> if not.
/// Gets the ID of the embed channel set in the widget settings of this guild, or <see langword="null"/> if none is set.
/// </summary>
ulong? EmbedChannelId { get; }
/// <summary>
/// Gets the ID of the channel where randomized welcome messages are sent if set, or <see langword="null"/> if not.
/// Gets the ID of the channel where randomized welcome messages are sent, or <see langword="null"/> if none is set.
/// </summary>
ulong? SystemChannelId { get; }
/// <summary>
@@ -106,57 +112,62 @@ namespace Discord
/// <summary>
/// Modifies this guild.
/// </summary>
/// <param name="func">The properties to modify the guild with.</param>
/// <param name="options">The options to be used when sending the request.</param>
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="options">The options to be used when sending the request.</param>
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="options">The options to be used when sending the request.</param>
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="options">The options to be used when sending the request.</param>
Task ReorderRolesAsync(IEnumerable<ReorderRoleProperties> args, RequestOptions options = null);
/// <summary>
/// Leaves this guild. If you are the owner, use
/// <see cref="IDeletable.DeleteAsync(Discord.RequestOptions)" /> instead.
/// 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>
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>
Task<IReadOnlyCollection<IBan>> GetBansAsync(RequestOptions options = null);
/// <summary>
/// Bans the provided <paramref name="user"/> from this guild and optionally prunes their recent messages.
/// Bans the provided user from this guild and optionally prunes their recent messages.
/// </summary>
/// <param name="user">
/// The user to ban.
/// </param>
/// <param name="user">The user to ban.</param>
/// <param name="pruneDays">
/// The number of days to remove messages from this <paramref name="user"/> for - must be between [0, 7]
/// </param>
/// <param name="reason">
/// The reason of the ban to be written in the audit log.
/// The number of days to remove messages from this <paramref name="user" /> for - must be between [0, 7].
/// </param>
/// <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>
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.
/// </summary>
/// <param name="userId">
/// The ID of the user to ban.
/// </param>
/// <param name="userId">The ID of the user to ban.</param>
/// <param name="pruneDays">
/// The number of days to remove messages from this user for - must be between [0, 7]
/// </param>
/// <param name="reason">
/// The reason of the ban to be written in the audit log.
/// The number of days to remove messages from this user for - must be between [0, 7].
/// </param>
/// <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>
Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null);
/// <summary>
/// Unbans the provided <paramref name="user"/> if they are currently banned.
/// Unbans the provided user if they are currently banned.
/// </summary>
Task RemoveBanAsync(IUser user, RequestOptions options = null);
/// <summary>
@@ -167,66 +178,114 @@ namespace Discord
/// <summary>
/// Gets a collection of all channels 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>
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, or <see langword="null" /> if not found.
/// </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>
Task<IGuildChannel> GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets a collection of all text channels 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>
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.
/// Gets a text channel in this guild with the provided ID, or <see langword="null" /> if not found.
/// </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>
Task<ITextChannel> GetTextChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets a collection of all voice channels 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>
Task<IReadOnlyCollection<IVoiceChannel>> GetVoiceChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets a collection of all category channels 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>
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, or <see langword="null" /> if not found.
/// </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>
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 voice AFK channel in this guild with the provided ID, or <see langword="null" /> if not found.
/// </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>
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
/// Gets the default system text channel in this guild with the provided ID, or <see langword="null" /> if
/// none is set.
/// </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>
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
/// Gets the top viewable text channel in this guild with the provided ID, or <see langword="null" /> if not
/// found.
/// </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>
Task<ITextChannel> GetDefaultChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets the embed channel in this guild.
/// 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.
/// </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>
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>
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>
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>
Task<ICategoryChannel> CreateCategoryAsync(string name, RequestOptions options = null);

Task<IReadOnlyCollection<IGuildIntegration>> GetIntegrationsAsync(RequestOptions options = null);
@@ -249,24 +308,33 @@ namespace Discord
/// <param name="permissions">The guild permission that the role should possess.</param>
/// <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>
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>
Task<IReadOnlyCollection<IGuildUser>> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); //TODO: shouldnt this be paged?
/// <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>
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.
/// </summary>
/// <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>
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>
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>
Task<IGuildUser> GetOwnerAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Downloads all users for this guild if the current list is incomplete.
@@ -274,11 +342,12 @@ namespace Discord
Task DownloadUsersAsync();
/// <summary>
/// Removes all users from this guild if they have not logged on in a provided number of
/// <paramref name="days"/> or, if <paramref name="simulate"/> is true, returns the number of users that
/// would be removed.
/// <paramref name="days" /> or, if <paramref name="simulate" /> is <see langword="true"/>, returns the
/// number of users that would be removed.
/// </summary>
/// <param name="days">The number of days required for the users to be kicked.</param>
/// <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.
/// </returns>
@@ -288,32 +357,41 @@ namespace Discord
/// Gets the webhook in this guild with the provided ID, or <see langword="null"/> if not found.
/// </summary>
/// <param name="id">The webhook ID.</param>
/// <param name="options">The options to be used when sending the request.</param>
Task<IWebhook> GetWebhookAsync(ulong id, RequestOptions options = null);
/// <summary>
/// Gets a collection of all webhooks from this guild.
/// Gets a collection of all webhook from this guild.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
Task<IReadOnlyCollection<IWebhook>> GetWebhooksAsync(RequestOptions options = null);
/// <summary>
/// Gets a specific emote from this guild.
/// </summary>
/// <param name="id">The guild emote ID.</param>
/// <param name="options">The options to be used when sending the request.</param>
Task<GuildEmote> GetEmoteAsync(ulong id, RequestOptions options = null);
/// <summary>
/// Creates a new emote in this guild.
/// Creates a new <see cref="GuildEmote"/> in this guild.
/// </summary>
/// <param name="name">The name of the guild emote.</param>
/// <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>
Task<GuildEmote> CreateEmoteAsync(string name, Image image, Optional<IEnumerable<IRole>> roles = default(Optional<IEnumerable<IRole>>), RequestOptions options = null);

/// <summary>
/// Modifies an existing <paramref name="emote"/> in this guild.
/// 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="options">The options to be used when sending the request.</param>
Task<GuildEmote> ModifyEmoteAsync(GuildEmote emote, Action<EmoteProperties> func, RequestOptions options = null);
/// <summary>
/// Deletes an existing <paramref name="emote"/> from this guild.
/// Deletes an existing <see cref="GuildEmote"/> from this guild.
/// </summary>
/// <param name="emote">The guild emote to delete.</param>
/// <param name="emote">The emote to delete.</param>
/// <param name="options">The options to be used when sending the request.</param>
Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null);
}
}

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

@@ -3,13 +3,14 @@ using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// Represents whether the object is deletable or not.
/// Determines whether the object is deletable or not.
/// </summary>
public interface IDeletable
{
/// <summary>
/// Deletes this object and all its children.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
Task DeleteAsync(RequestOptions options = null);
}
}

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

@@ -1,13 +1,16 @@
namespace Discord
{
/// <summary>
/// Represents whether the object is mentionable or not.
/// Determines whether the object is mentionable or not.
/// </summary>
public interface IMentionable
{
/// <summary>
/// Returns a special string used to mention this object.
/// </summary>
/// <returns>
/// A string that is recognized by Discord as a mention (e.g. &lt;@168693960628371456&gt;).
/// </returns>
string Mention { get; }
}
}

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

@@ -1,3 +1,4 @@
using System;
using System.IO;
namespace Discord
{
@@ -11,7 +12,7 @@ namespace Discord
/// </summary>
public Stream Stream { get; }
/// <summary>
/// Create the image with a <see cref="System.IO.Stream" /> .
/// Create the image with a <see cref="System.IO.Stream" />.
/// </summary>
/// <param name="stream">
/// The <see cref="System.IO.Stream" /> to create the image with. Note that this must be some type of stream
@@ -26,10 +27,30 @@ namespace Discord
/// Create the image from a file path.
/// </summary>
/// <remarks>
/// This file <paramref name="path" /> is NOT validated, and is passed directly into a
/// <see cref="File.OpenRead" />
/// This file path is NOT validated and is passed directly into a
/// <see cref="File.OpenRead"/>.
/// </remarks>
/// <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" /> .
/// </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
/// characters.
/// </exception>
/// <exception cref="NotSupportedException"><paramref name="path" /> is in an invalid format.</exception>
/// <exception cref="DirectoryNotFoundException">
/// The specified <paramref name="path"/> is invalid, (for example, it is on an unmapped drive).
/// </exception>
/// <exception cref="UnauthorizedAccessException">
/// <paramref name="path" /> specified a directory.-or- The caller does not have the required permission.
/// </exception>
/// <exception cref="FileNotFoundException">The file specified in <paramref name="path" /> was not found.
/// </exception>
/// <exception cref="IOException">An I/O error occurred while opening the file. </exception>
public Image(string path)
{
Stream = File.OpenRead(path);


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

@@ -41,6 +41,9 @@ namespace Discord
}

/// <summary> Gets or sets the title of an <see cref="Embed"/>. </summary>
/// <exception cref="ArgumentException" accessor="set">Title length exceeds the maximum allowed by Discord.
/// </exception>
/// <returns> The title of the embed.</returns>
public string Title
{
get => _title;
@@ -50,7 +53,10 @@ namespace Discord
_title = value;
}
}

/// <summary> Gets or sets the description of an <see cref="Embed"/>. </summary>
/// <exception cref="ArgumentException" accessor="set">Description length exceeds the maximum allowed by Discord.</exception>
/// <returns> The description of the embed.</returns>
public string Description
{
get => _description;
@@ -62,6 +68,8 @@ namespace Discord
}

/// <summary> Gets or sets the URL of an <see cref="Embed"/>. </summary>
/// <exception cref="ArgumentException" accessor="set">Url is not a well-formed <see cref="Uri"/>.</exception>
/// <returns> The URL of the embed.</returns>
public string Url
{
get => _url;
@@ -72,6 +80,8 @@ namespace Discord
}
}
/// <summary> Gets or sets the thumbnail URL of an <see cref="Embed"/>. </summary>
/// <exception cref="ArgumentException" accessor="set">Url is not a well-formed <see cref="Uri"/>.</exception>
/// <returns> The thumbnail URL of the embed.</returns>
public string ThumbnailUrl
{
get => _thumbnail?.Url;
@@ -82,6 +92,8 @@ namespace Discord
}
}
/// <summary> Gets or sets the image URL of an <see cref="Embed"/>. </summary>
/// <exception cref="ArgumentException" accessor="set">Url is not a well-formed <see cref="Uri"/>.</exception>
/// <returns> The image URL of the embed.</returns>
public string ImageUrl
{
get => _image?.Url;
@@ -91,7 +103,13 @@ namespace Discord
_image = new EmbedImage(value, null, null, null);
}
}

/// <summary> Gets or sets the list of <see cref="EmbedFieldBuilder"/> of an <see cref="Embed"/>. </summary>
/// <exception cref="ArgumentNullException" accessor="set">An embed builder's fields collection is set to
/// <see langword="null"/>.</exception>
/// <exception cref="ArgumentException" accessor="set">Description length exceeds the maximum allowed by
/// Discord.</exception>
/// <returns> The list of existing <see cref="EmbedFieldBuilder"/>.</returns>
public List<EmbedFieldBuilder> Fields
{
get => _fields;
@@ -103,18 +121,42 @@ namespace Discord
}
}

/// <summary> Gets or sets the timestamp of an <see cref="Embed"/>. </summary>
/// <summary>
/// Gets or sets the timestamp of an <see cref="Embed" />.
/// </summary>
/// <returns>
/// The timestamp of the embed, or <see langword="null" /> if none is set.
/// </returns>
public DateTimeOffset? Timestamp { get; set; }
/// <summary> Gets or sets the sidebar color of an <see cref="Embed"/>. </summary>
/// <summary>
/// Gets or sets the sidebar color of an <see cref="Embed" />.
/// </summary>
/// <returns>
/// The color of the embed, or <see langword="null" /> if none is set.
/// </returns>
public Color? Color { get; set; }
/// <summary> Gets or sets the <see cref="EmbedAuthorBuilder"/> of an <see cref="Embed"/>. </summary>
/// <summary>
/// Gets or sets the <see cref="EmbedAuthorBuilder" /> of an <see cref="Embed" />.
/// </summary>
/// <returns>
/// The author field builder of the embed, or <see langword="null" /> if none is set.
/// </returns>
public EmbedAuthorBuilder Author { get; set; }
/// <summary> Gets or sets the <see cref="EmbedFooterBuilder"/> of an <see cref="Embed"/>. </summary>
/// <summary>
/// Gets or sets the <see cref="EmbedFooterBuilder" /> of an <see cref="Embed" />.
/// </summary>
/// <returns>
/// The footer field builder of the embed, or <see langword="null" /> if none is set.
/// </returns>
public EmbedFooterBuilder Footer { get; set; }

/// <summary>
/// Gets the total length of all embed properties.
/// </summary>
/// <returns>
/// The combined length of <see cref="Title"/>, <see cref="EmbedAuthor.Name"/>, <see cref="Description"/>,
/// <see cref="EmbedFooter.Text"/>, <see cref="EmbedField.Name"/>, and <see cref="EmbedField.Value"/>.
/// </returns>
public int Length
{
get
@@ -130,9 +172,12 @@ 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>
/// The current builder.
/// </returns>
public EmbedBuilder WithTitle(string title)
{
Title = title;
@@ -142,6 +187,9 @@ namespace Discord
/// Sets the description of an <see cref="Embed"/>.
/// </summary>
/// <param name="description"> The description to be set. </param>
/// <returns>
/// The current builder.
/// </returns>
public EmbedBuilder WithDescription(string description)
{
Description = description;
@@ -151,6 +199,9 @@ namespace Discord
/// Sets the URL of an <see cref="Embed"/>.
/// </summary>
/// <param name="url"> The URL to be set. </param>
/// <returns>
/// The current builder.
/// </returns>
public EmbedBuilder WithUrl(string url)
{
Url = url;
@@ -160,15 +211,21 @@ namespace Discord
/// Sets the thumbnail URL of an <see cref="Embed"/>.
/// </summary>
/// <param name="thumbnailUrl"> The thumbnail URL to be set. </param>
/// <returns>
/// The current builder.
/// </returns>
public EmbedBuilder WithThumbnailUrl(string thumbnailUrl)
{
ThumbnailUrl = thumbnailUrl;
return this;
}
/// <summary>
/// Sets the image URL of an <see cref="Embed" /> .
/// Sets the image URL of an <see cref="Embed" />.
/// </summary>
/// <param name="imageUrl">The image URL to be set.</param>
/// <returns>
/// The current builder.
/// </returns>
public EmbedBuilder WithImageUrl(string imageUrl)
{
ImageUrl = imageUrl;
@@ -177,24 +234,33 @@ namespace Discord
/// <summary>
/// Sets the timestamp of an <see cref="Embed" /> to the current time.
/// </summary>
/// <returns>
/// The current builder.
/// </returns>
public EmbedBuilder WithCurrentTimestamp()
{
Timestamp = DateTimeOffset.UtcNow;
return this;
}
/// <summary>
/// Sets the timestamp of an <see cref="Embed" /> .
/// Sets the timestamp of an <see cref="Embed" />.
/// </summary>
/// <param name="dateTimeOffset">The timestamp to be set.</param>
/// <returns>
/// The current builder.
/// </returns>
public EmbedBuilder WithTimestamp(DateTimeOffset dateTimeOffset)
{
Timestamp = dateTimeOffset;
return this;
}
/// <summary>
/// Sets the sidebar color of an <see cref="Embed" /> .
/// Sets the sidebar color of an <see cref="Embed" />.
/// </summary>
/// <param name="color">The color to be set.</param>
/// <returns>
/// The current builder.
/// </returns>
public EmbedBuilder WithColor(Color color)
{
Color = color;
@@ -202,9 +268,12 @@ namespace Discord
}

/// <summary>
/// Sets the <see cref="EmbedAuthorBuilder" /> of an <see cref="Embed" /> .
/// Sets the <see cref="EmbedAuthorBuilder" /> of an <see cref="Embed" />.
/// </summary>
/// <param name="author">The author builder class containing the author field properties.</param>
/// <returns>
/// The current builder.
/// </returns>
public EmbedBuilder WithAuthor(EmbedAuthorBuilder author)
{
Author = author;
@@ -214,6 +283,9 @@ namespace Discord
/// Sets the author field of an <see cref="Embed" /> with the provided properties.
/// </summary>
/// <param name="action">The <see langword="delegate"/> containing the author field properties.</param>
/// <returns>
/// The current builder.
/// </returns>
public EmbedBuilder WithAuthor(Action<EmbedAuthorBuilder> action)
{
var author = new EmbedAuthorBuilder();
@@ -227,6 +299,9 @@ namespace Discord
/// <param name="name">The title of the author field.</param>
/// <param name="iconUrl">The icon URL of the author field.</param>
/// <param name="url">The URL of the author field.</param>
/// <returns>
/// The current builder.
/// </returns>
public EmbedBuilder WithAuthor(string name, string iconUrl = null, string url = null)
{
var author = new EmbedAuthorBuilder
@@ -239,9 +314,12 @@ namespace Discord
return this;
}
/// <summary>
/// Sets the <see cref="EmbedFooterBuilder" /> of an <see cref="Embed" /> .
/// Sets the <see cref="EmbedFooterBuilder" /> of an <see cref="Embed" />.
/// </summary>
/// <param name="footer">The footer builder class containing the footer field properties.</param>
/// <returns>
/// The current builder.
/// </returns>
public EmbedBuilder WithFooter(EmbedFooterBuilder footer)
{
Footer = footer;
@@ -251,6 +329,9 @@ namespace Discord
/// Sets the footer field of an <see cref="Embed" /> with the provided properties.
/// </summary>
/// <param name="action">The <see langword="delegate"/> containing the footer field properties.</param>
/// <returns>
/// The current builder.
/// </returns>
public EmbedBuilder WithFooter(Action<EmbedFooterBuilder> action)
{
var footer = new EmbedFooterBuilder();
@@ -263,6 +344,9 @@ namespace Discord
/// </summary>
/// <param name="text">The title of the footer field.</param>
/// <param name="iconUrl">The icon URL of the footer field.</param>
/// <returns>
/// The current builder.
/// </returns>
public EmbedBuilder WithFooter(string text, string iconUrl = null)
{
var footer = new EmbedFooterBuilder
@@ -280,6 +364,9 @@ namespace Discord
/// <param name="name">The title of the field.</param>
/// <param name="value">The value of the field.</param>
/// <param name="inline">Indicates whether the field is in-line or not.</param>
/// <returns>
/// The current builder.
/// </returns>
public EmbedBuilder AddField(string name, object value, bool inline = false)
{
var field = new EmbedFieldBuilder()
@@ -289,11 +376,16 @@ namespace Discord
AddField(field);
return this;
}

/// <summary>
/// Adds a field with the provided <see cref="EmbedFieldBuilder" /> to an
/// <see cref="Embed" />.
/// </summary>
/// <param name="field">The field builder class containing the field properties.</param>
/// <exception cref="ArgumentException">Field count exceeds the maximum allowed by Discord.</exception>
/// <returns>
/// The current builder.
/// </returns>
public EmbedBuilder AddField(EmbedFieldBuilder field)
{
if (Fields.Count >= MaxFieldCount)
@@ -308,6 +400,9 @@ namespace Discord
/// Adds an <see cref="Embed" /> field with the provided properties.
/// </summary>
/// <param name="action">The <see langword="delegate"/> containing the field properties.</param>
/// <returns>
/// The current builder.
/// </returns>
public EmbedBuilder AddField(Action<EmbedFieldBuilder> action)
{
var field = new EmbedFieldBuilder();
@@ -317,11 +412,12 @@ namespace Discord
}

/// <summary>
/// Builds the <see cref="Embed" /> into a Rich Embed format.
/// Builds the <see cref="Embed" /> into a Rich Embed ready to be sent.
/// </summary>
/// <returns>
/// The built embed object.
/// </returns>
/// <exception cref="InvalidOperationException">Total embed length exceeds the maximum allowed by Discord.</exception>
public Embed Build()
{
if (Length > MaxEmbedLength)
@@ -335,6 +431,9 @@ namespace Discord
}
}

/// <summary>
/// Represents a builder class for an embed field.
/// </summary>
public class EmbedFieldBuilder
{
private string _name;
@@ -352,6 +451,14 @@ namespace Discord
/// <summary>
/// Gets or sets the field name.
/// </summary>
/// <exception cref="ArgumentException">
/// <para>Field name is <see langword="null" />, empty or entirely whitespace.</para>
/// <para><c>- or -</c></para>
/// <para>Field name length exceeds <see cref="MaxFieldNameLength"/>.</para>
/// </exception>
/// <returns>
/// The name of the field.
/// </returns>
public string Name
{
get => _name;
@@ -366,19 +473,27 @@ namespace Discord
/// <summary>
/// Gets or sets the field value.
/// </summary>
/// <exception cref="ArgumentException" accessor="set">
/// <para>Field value is <see langword="null" />, empty or entirely whitespace.</para>
/// <para><c>- or -</c></para>
/// <para>Field value length exceeds <see cref="MaxFieldValueLength"/>.</para>
/// </exception>
/// <returns>
/// The value of the field.
/// </returns>
public object Value
{
get => _value;
set
{
var stringValue = value?.ToString();
if (string.IsNullOrEmpty(stringValue)) throw new ArgumentException($"Field value must not be null or empty.", nameof(Value));
if (string.IsNullOrEmpty(stringValue)) throw new ArgumentException("Field value must not be null or empty.", nameof(Value));
if (stringValue.Length > MaxFieldValueLength) throw new ArgumentException($"Field value length must be less than or equal to {MaxFieldValueLength}.", nameof(Value));
_value = stringValue;
}
}
/// <summary>
/// Gets or sets whether the field should be in-line with each other.
/// Determines whether the field should be in-line with each other.
/// </summary>
public bool IsInline { get; set; }

@@ -386,6 +501,9 @@ namespace Discord
/// Sets the field name.
/// </summary>
/// <param name="name">The name to set the field name to.</param>
/// <returns>
/// The current builder.
/// </returns>
public EmbedFieldBuilder WithName(string name)
{
Name = name;
@@ -395,14 +513,20 @@ namespace Discord
/// Sets the field value.
/// </summary>
/// <param name="value">The value to set the field value to.</param>
/// <returns>
/// The current builder.
/// </returns>
public EmbedFieldBuilder WithValue(object value)
{
Value = value;
return this;
}
/// <summary>
/// Sets whether the field should be in-line with each other.
/// Determines whether the field should be in-line with each other.
/// </summary>
/// <returns>
/// The current builder.
/// </returns>
public EmbedFieldBuilder WithIsInline(bool isInline)
{
IsInline = isInline;
@@ -410,19 +534,42 @@ namespace Discord
}

/// <summary>
/// Builds the field builder into a <see cref="EmbedField"/> class.
/// Builds the field builder into a <see cref="EmbedField" /> class.
/// </summary>
/// <returns>
/// The current builder.
/// </returns>
/// <exception cref="ArgumentException">
/// <para><see cref="Name"/> or <see cref="Value"/> is <see langword="null" />, empty or entirely whitespace.</para>
/// <para><c>- or -</c></para>
/// <para><see cref="Name"/> or <see cref="Value"/> length exceeds the maximum allowed by Discord.</para>
/// </exception>
public EmbedField Build()
=> new EmbedField(Name, Value.ToString(), IsInline);
}

/// <summary>
/// Represents a builder class for a author field.
/// </summary>
public class EmbedAuthorBuilder
{
private string _name;
private string _url;
private string _iconUrl;
/// <summary>
/// Gets the maximum author name length allowed by Discord.
/// </summary>
public const int MaxAuthorNameLength = 256;

/// <summary>
/// Gets or sets the author name.
/// </summary>
/// <exception cref="ArgumentException">
/// Author name length is longer than <see cref="MaxAuthorNameLength" />.
/// </exception>
/// <returns>
/// The author name.
/// </returns>
public string Name
{
get => _name;
@@ -432,6 +579,13 @@ namespace Discord
_name = value;
}
}
/// <summary>
/// Gets or sets the URL of the author field.
/// </summary>
/// <exception cref="ArgumentException" accessor="set">Url is not a well-formed <see cref="Uri"/>.</exception>
/// <returns>
/// The URL of the author field.
/// </returns>
public string Url
{
get => _url;
@@ -441,6 +595,13 @@ namespace Discord
_url = value;
}
}
/// <summary>
/// Gets or sets the icon URL of the author field.
/// </summary>
/// <exception cref="ArgumentException" accessor="set">Url is not a well-formed <see cref="Uri"/>.</exception>
/// <returns>
/// The icon URL of the author field.
/// </returns>
public string IconUrl
{
get => _iconUrl;
@@ -451,33 +612,82 @@ namespace Discord
}
}

/// <summary>
/// Sets the name of the author field.
/// </summary>
/// <param name="name">The name of the author field.</param>
/// <returns>
/// The current builder.
/// </returns>
public EmbedAuthorBuilder WithName(string name)
{
Name = name;
return this;
}
/// <summary>
/// Sets the URL of the author field.
/// </summary>
/// <param name="url">The URL of the author field.</param>
/// <returns>
/// The current builder.
/// </returns>
public EmbedAuthorBuilder WithUrl(string url)
{
Url = url;
return this;
}
/// <summary>
/// Sets the icon URL of the author field.
/// </summary>
/// <param name="iconUrl">The icon URL of the author field.</param>
/// <returns>
/// The current builder.
/// </returns>
public EmbedAuthorBuilder WithIconUrl(string iconUrl)
{
IconUrl = iconUrl;
return this;
}

/// <summary>
/// Builds the author field to be used.
/// </summary>
/// <exception cref="ArgumentException">
/// <para>Author name length is longer than <see cref="MaxAuthorNameLength" />.</para>
/// <para><c>- or -</c></para>
/// <para><see cref="Url"/> is not a well-formed <see cref="Uri" />.</para>
/// <para><c>- or -</c></para>
/// <para><see cref="IconUrl"/> is not a well-formed <see cref="Uri" />.</para>
/// </exception>
/// <returns>
/// The built author field.
/// </returns>
public EmbedAuthor Build()
=> new EmbedAuthor(Name, Url, IconUrl, null);
}

/// <summary>
/// Represents a builder class for an embed footer.
/// </summary>
public class EmbedFooterBuilder
{
private string _text;
private string _iconUrl;

/// <summary>
/// Gets the maximum footer length allowed by Discord.
/// </summary>
public const int MaxFooterTextLength = 2048;

/// <summary>
/// Gets or sets the footer text.
/// </summary>
/// <exception cref="ArgumentException">
/// Author name length is longer than <see cref="MaxFooterTextLength" />.
/// </exception>
/// <returns>
/// The footer text.
/// </returns>
public string Text
{
get => _text;
@@ -487,6 +697,13 @@ namespace Discord
_text = value;
}
}
/// <summary>
/// Gets or sets the icon URL of the footer field.
/// </summary>
/// <exception cref="ArgumentException" accessor="set">Url is not a well-formed <see cref="Uri"/>.</exception>
/// <returns>
/// The icon URL of the footer field.
/// </returns>
public string IconUrl
{
get => _iconUrl;
@@ -497,17 +714,43 @@ namespace Discord
}
}

/// <summary>
/// Sets the name of the footer field.
/// </summary>
/// <param name="text">The text of the footer field.</param>
/// <returns>
/// The current builder.
/// </returns>
public EmbedFooterBuilder WithText(string text)
{
Text = text;
return this;
}
/// <summary>
/// Sets the icon URL of the footer field.
/// </summary>
/// <param name="iconUrl">The icon URL of the footer field.</param>
/// <returns>
/// The current builder.
/// </returns>
public EmbedFooterBuilder WithIconUrl(string iconUrl)
{
IconUrl = iconUrl;
return this;
}

/// <summary>
/// Builds the footer field to be used.
/// </summary>
/// <returns></returns>
/// <exception cref="ArgumentException">
/// <para><see cref="Text"/> length is longer than <see cref="MaxFooterTextLength" />.</para>
/// <para><c>- or -</c></para>
/// <para><see cref="IconUrl"/> is not a well-formed <see cref="Uri"/>.</para>
/// </exception>
/// <returns>
/// A built footer field.
/// </returns>
public EmbedFooter Build()
=> new EmbedFooter(Text, IconUrl, null);
}


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

@@ -17,7 +17,7 @@ namespace Discord
/// </summary>
public string Value { get; internal set; }
/// <summary>
/// Gets whether the field should be in-line with each other.
/// Determines whether the field should be in-line with each other.
/// </summary>
public bool Inline { get; internal set; }



+ 3
- 0
src/Discord.Net.Core/Entities/Roles/Color.cs View File

@@ -83,12 +83,14 @@ namespace Discord
((uint)g << 8) |
(uint)b;
}

/// <summary>
/// Initializes a <see cref="Color"/> struct with the given RGB value.
/// </summary>
/// <param name="r">The value that represents the red color. Must be within 0~255.</param>
/// <param name="g">The value that represents the green color. Must be within 0~255.</param>
/// <param name="b">The value that represents the blue color. Must be within 0~255.</param>
/// <exception cref="ArgumentOutOfRangeException">The argument value is not between 0 to 255.</exception>
public Color(int r, int g, int b)
{
if (r < 0 || r > 255)
@@ -108,6 +110,7 @@ namespace Discord
/// <param name="r">The value that represents the red color. Must be within 0~1.</param>
/// <param name="g">The value that represents the green color. Must be within 0~1.</param>
/// <param name="b">The value that represents the blue color. Must be within 0~1.</param>
/// <exception cref="ArgumentOutOfRangeException">The argument value is not between 0 to 1.</exception>
public Color(float r, float g, float b)
{
if (r < 0.0f || r > 1.0f)


+ 17
- 3
src/Discord.Net.Core/Entities/Roles/IRole.cs View File

@@ -18,16 +18,28 @@ namespace Discord
/// </summary>
Color Color { get; }
/// <summary>
/// Returns <see langword="true"/> if users of this role are separated in the user list.
/// Determines whether the role can be separated in the user list.
/// </summary>
/// <returns>
/// Returns <see langword="true"/> if users of this role are separated in the user list; otherwise, returns
/// <see langword="false"/>.
/// </returns>
bool IsHoisted { get; }
/// <summary>
/// Returns <see langword="true"/> if this role is automatically managed by Discord.
/// Determines whether the role is managed by Discord.
/// </summary>
/// <returns>
/// Returns <see langword="true"/> if this role is automatically managed by Discord; otherwise, returns
/// <see langword="false"/>.
/// </returns>
bool IsManaged { get; }
/// <summary>
/// Returns <see langword="true"/> if this role may be mentioned in messages.
/// Determines whether the role is mentionable.
/// </summary>
/// <returns>
/// Returns <see langword="true"/> if this role may be mentioned in messages; otherwise, returns
/// <see langword="false"/>.
/// </returns>
bool IsMentionable { get; }
/// <summary>
/// Gets the name of this role.
@@ -45,6 +57,8 @@ namespace Discord
/// <summary>
/// Modifies this role.
/// </summary>
/// <param name="func">The properties to modify the role with.</param>
/// <param name="options">The options to be used when sending the request.</param>
Task ModifyAsync(Action<RoleProperties> func, RequestOptions options = null);
}
}

+ 8
- 1
src/Discord.Net.Core/Entities/Users/IGuildUser.cs View File

@@ -5,7 +5,7 @@ using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// Represents a Discord user that is in a guild.
/// Represents a generic guild user.
/// </summary>
public interface IGuildUser : IUser, IVoiceState
{
@@ -46,31 +46,38 @@ namespace Discord
/// Kicks this user from this guild.
/// </summary>
/// <param name="reason">The reason for the kick which will be recorded in the audit log.</param>
/// <param name="options">The options to be used when sending the request.</param>
Task KickAsync(string reason = null, RequestOptions options = null);
/// <summary>
/// Modifies this user's properties in this guild.
/// </summary>
/// <param name="func">The properties to modify the user with.</param>
/// <param name="options">The options to be used when sending the request.</param>
Task ModifyAsync(Action<GuildUserProperties> func, RequestOptions options = null);

/// <summary>
/// Adds a <paramref name="role"/> to this user in this guild.
/// </summary>
/// <param name="role">The role to be added to the user.</param>
/// <param name="options">The options to be used when sending the request.</param>
Task AddRoleAsync(IRole role, RequestOptions options = null);
/// <summary>
/// Adds <paramref name="roles"/> to this user in this guild.
/// </summary>
/// <param name="roles">The roles to be added to the user.</param>
/// <param name="options">The options to be used when sending the request.</param>
Task AddRolesAsync(IEnumerable<IRole> roles, RequestOptions options = null);
/// <summary>
/// Removes a <paramref name="role"/> from this user in this guild.
/// </summary>
/// <param name="role">The role to be removed from the user.</param>
/// <param name="options">The options to be used when sending the request.</param>
Task RemoveRoleAsync(IRole role, RequestOptions options = null);
/// <summary>
/// Removes <paramref name="roles"/> from this user in this guild.
/// </summary>
/// <param name="roles">The roles to be removed from the user.</param>
/// <param name="options">The options to be used when sending the request.</param>
Task RemoveRolesAsync(IEnumerable<IRole> roles, RequestOptions options = null);
}
}

+ 2
- 3
src/Discord.Net.Core/Entities/Users/IUser.cs View File

@@ -3,7 +3,7 @@ using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// Represents a Discord user.
/// Represents a generic user.
/// </summary>
public interface IUser : ISnowflakeEntity, IMentionable, IPresence
{
@@ -41,8 +41,7 @@ namespace Discord
string Username { get; }

/// <summary>
/// Returns a private message channel to this user, creating one if it does not already
/// exist.
/// Returns a direct message channel to this user, or create one if it does not already exist.
/// </summary>
Task<IDMChannel> GetOrCreateDMChannelAsync(RequestOptions options = null);
}


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

@@ -26,7 +26,7 @@ namespace Discord
/// </summary>
bool IsSuppressed { get; }
/// <summary>
/// Gets the voice channel this user is currently in, if any.
/// Gets the voice channel this user is currently in, or <see langword="null"/> if none.
/// </summary>
IVoiceChannel VoiceChannel { get; }
/// <summary>


+ 1
- 0
src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs View File

@@ -30,6 +30,7 @@ namespace Discord
builder.WithAuthor($"{user.Nickname ?? user.Username}#{user.Discriminator}", user.GetAvatarUrl());

/// <summary> Converts a <see cref="EmbedType.Rich"/> <see cref="IEmbed"/> object to a <see cref="EmbedBuilder"/>. </summary>
/// <exception cref="InvalidOperationException">The embed type is not <see cref="EmbedType.Rich"/>.</exception>
public static EmbedBuilder ToEmbedBuilder(this IEmbed embed)
{
if (embed.Type != EmbedType.Rich)


+ 29
- 0
src/Discord.Net.Core/IDiscordClient.cs View File

@@ -5,6 +5,9 @@ using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents a generic Discord client.
/// </summary>
public interface IDiscordClient : IDisposable
{
ConnectionState ConnectionState { get; }
@@ -14,11 +17,37 @@ namespace Discord
Task StartAsync();
Task StopAsync();

/// <summary>
/// Gets the application information associated with this account.
/// </summary>
Task<IApplication> GetApplicationInfoAsync(RequestOptions options = null);

/// <summary>
/// Gets a generic channel with the provided ID.
/// </summary>
/// <param name="id">The ID of the channel.</param>
/// <param name="mode">The <see cref="CacheMode"/> that determines whether the object should be fetched from cache.</param>
Task<IChannel> GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets a list of private channels.
/// </summary>
/// <param name="mode">
/// The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.
/// </param>
Task<IReadOnlyCollection<IPrivateChannel>> GetPrivateChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets a list of direct message channels.
/// </summary>
/// <param name="mode">
/// The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.
/// </param>
Task<IReadOnlyCollection<IDMChannel>> GetDMChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets a list of group channels.
/// </summary>
/// <param name="mode">
/// The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.
/// </param>
Task<IReadOnlyCollection<IGroupChannel>> GetGroupChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);

Task<IReadOnlyCollection<IConnection>> GetConnectionsAsync(RequestOptions options = null);


+ 9
- 3
src/Discord.Net.Core/Logging/LogManager.cs View File

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

namespace Discord.Logging
@@ -24,7 +24,10 @@ namespace Discord.Logging
if (severity <= Level)
await _messageEvent.InvokeAsync(new LogMessage(severity, source, null, ex)).ConfigureAwait(false);
}
catch { }
catch
{
// ignored
}
}
public async Task LogAsync(LogSeverity severity, string source, string message, Exception ex = null)
{
@@ -33,7 +36,10 @@ namespace Discord.Logging
if (severity <= Level)
await _messageEvent.InvokeAsync(new LogMessage(severity, source, message, ex)).ConfigureAwait(false);
}
catch { }
catch
{
// ignored
}
}
#if FORMATSTR
public async Task LogAsync(LogSeverity severity, string source, FormattableString message, Exception ex = null)


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

@@ -44,7 +44,7 @@ namespace Discord

/// <summary>
/// Initializes a new <see cref="RequestOptions" /> class with the default request timeout set in
/// <see cref="DiscordConfig" /> .
/// <see cref="DiscordConfig" />.
/// </summary>
public RequestOptions()
{


+ 14
- 11
src/Discord.Net.Core/Utils/MentionUtils.cs View File

@@ -31,11 +31,12 @@ namespace Discord
/// <summary>
/// Parses a provided user mention string.
/// </summary>
/// <exception cref="ArgumentException">Invalid mention format.</exception>
public static ulong ParseUser(string text)
{
if (TryParseUser(text, out ulong id))
return id;
throw new ArgumentException("Invalid mention format", nameof(text));
throw new ArgumentException("Invalid mention format.", nameof(text));
}
/// <summary>
/// Tries to parse a provided user mention string.
@@ -59,11 +60,12 @@ namespace Discord
/// <summary>
/// Parses a provided channel mention string.
/// </summary>
/// <exception cref="ArgumentException">Invalid mention format.</exception>
public static ulong ParseChannel(string text)
{
if (TryParseChannel(text, out ulong id))
return id;
throw new ArgumentException("Invalid mention format", nameof(text));
throw new ArgumentException("Invalid mention format.", nameof(text));
}
/// <summary>
/// Tries to parse a provided channel mention string.
@@ -84,11 +86,12 @@ namespace Discord
/// <summary>
/// Parses a provided role mention string.
/// </summary>
/// <exception cref="ArgumentException">Invalid mention format.</exception>
public static ulong ParseRole(string text)
{
if (TryParseRole(text, out ulong id))
return id;
throw new ArgumentException("Invalid mention format", nameof(text));
throw new ArgumentException("Invalid mention format.", nameof(text));
}
/// <summary>
/// Tries to parse a provided role mention string.
@@ -163,22 +166,22 @@ namespace Discord
if (user != null)
return $"@{guildUser?.Nickname ?? user?.Username}";
else
return $"";
return "";
case TagHandling.NameNoPrefix:
if (user != null)
return $"{guildUser?.Nickname ?? user?.Username}";
else
return $"";
return "";
case TagHandling.FullName:
if (user != null)
return $"@{user.Username}#{user.Discriminator}";
else
return $"";
return "";
case TagHandling.FullNameNoPrefix:
if (user != null)
return $"{user.Username}#{user.Discriminator}";
else
return $"";
return "";
case TagHandling.Sanitize:
if (guildUser != null && guildUser.Nickname == null)
return MentionUser($"{SanitizeChar}{tag.Key}", false);
@@ -200,13 +203,13 @@ namespace Discord
if (channel != null)
return $"#{channel.Name}";
else
return $"";
return "";
case TagHandling.NameNoPrefix:
case TagHandling.FullNameNoPrefix:
if (channel != null)
return $"{channel.Name}";
else
return $"";
return "";
case TagHandling.Sanitize:
return MentionChannel($"{SanitizeChar}{tag.Key}");
}
@@ -225,13 +228,13 @@ namespace Discord
if (role != null)
return $"@{role.Name}";
else
return $"";
return "";
case TagHandling.NameNoPrefix:
case TagHandling.FullNameNoPrefix:
if (role != null)
return $"{role.Name}";
else
return $"";
return "";
case TagHandling.Sanitize:
return MentionRole($"{SanitizeChar}{tag.Key}");
}


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

@@ -1,4 +1,4 @@
using System;
using System;

namespace Discord
{
@@ -86,7 +86,7 @@ namespace Discord

private static ArgumentException CreateNotEqualException<T>(string name, string msg, T value)
{
if (msg == null) return new ArgumentException($"Value may not be equal to {value}", name);
if (msg == null) return new ArgumentException($"Value may not be equal to {value}.", name);
else return new ArgumentException(msg, name);
}

@@ -109,7 +109,7 @@ namespace Discord

private static ArgumentException CreateAtLeastException<T>(string name, string msg, T value)
{
if (msg == null) return new ArgumentException($"Value must be at least {value}", name);
if (msg == null) return new ArgumentException($"Value must be at least {value}.", name);
else return new ArgumentException(msg, name);
}

@@ -132,7 +132,7 @@ namespace Discord

private static ArgumentException CreateGreaterThanException<T>(string name, string msg, T value)
{
if (msg == null) return new ArgumentException($"Value must be greater than {value}", name);
if (msg == null) return new ArgumentException($"Value must be greater than {value}.", name);
else return new ArgumentException(msg, name);
}

@@ -155,7 +155,7 @@ namespace Discord

private static ArgumentException CreateAtMostException<T>(string name, string msg, T value)
{
if (msg == null) return new ArgumentException($"Value must be at most {value}", name);
if (msg == null) return new ArgumentException($"Value must be at most {value}.", name);
else return new ArgumentException(msg, name);
}

@@ -178,11 +178,12 @@ namespace Discord

private static ArgumentException CreateLessThanException<T>(string name, string msg, T value)
{
if (msg == null) return new ArgumentException($"Value must be less than {value}", name);
if (msg == null) return new ArgumentException($"Value must be less than {value}.", name);
else return new ArgumentException(msg, name);
}

// Bulk Delete
/// <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)));
@@ -193,12 +194,13 @@ 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>
public static void NotEveryoneRole(ulong[] roles, ulong guildId, string name)
{
for (var i = 0; i < roles.Length; i++)
{
if (roles[i] == guildId)
throw new ArgumentException($"The everyone role cannot be assigned to a user", name);
throw new ArgumentException("The everyone role cannot be assigned to a user.", name);
}
}
}


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

@@ -24,11 +24,20 @@ namespace Discord.Rest

internal API.DiscordRestApiClient ApiClient { get; }
internal LogManager LogManager { get; }
/// <summary>
/// Gets the login state of the client.
/// </summary>
public LoginState LoginState { get; private set; }
/// <summary>
/// Gets the logged-in user.
/// </summary>
public ISelfUser CurrentUser { get; protected set; }
/// <summary>
/// Gets the type of the authentication token.
/// </summary>
public TokenType TokenType => ApiClient.AuthTokenType;
/// <summary> Creates a new REST-only discord client. </summary>
/// <summary> Creates a new REST-only Discord client. </summary>
internal BaseDiscordClient(DiscordRestConfig config, API.DiscordRestApiClient client)
{
ApiClient = client;
@@ -48,8 +57,7 @@ namespace Discord.Rest
};
ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false);
}

/// <inheritdoc />
public async Task LoginAsync(TokenType tokenType, string token, bool validateToken = true)
{
await _stateLock.WaitAsync().ConfigureAwait(false);
@@ -87,8 +95,7 @@ namespace Discord.Rest
}
internal virtual Task OnLoginAsync(TokenType tokenType, string token)
=> Task.Delay(0);

/// <inheritdoc />
public async Task LogoutAsync()
{
await _stateLock.WaitAsync().ConfigureAwait(false);
@@ -130,49 +137,68 @@ namespace Discord.Rest
=> ClientHelper.GetRecommendShardCountAsync(this, options);

//IDiscordClient
/// <inheritdoc />
ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected;
/// <inheritdoc />
ISelfUser IDiscordClient.CurrentUser => CurrentUser;

/// <inheritdoc />
Task<IApplication> IDiscordClient.GetApplicationInfoAsync(RequestOptions options)
=> throw new NotSupportedException();

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

/// <inheritdoc />
Task<IReadOnlyCollection<IConnection>> IDiscordClient.GetConnectionsAsync(RequestOptions options)
=> Task.FromResult<IReadOnlyCollection<IConnection>>(ImmutableArray.Create<IConnection>());

/// <inheritdoc />
Task<IInvite> IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options)
=> Task.FromResult<IInvite>(null);

/// <inheritdoc />
Task<IGuild> IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IGuild>(null);
/// <inheritdoc />
Task<IReadOnlyCollection<IGuild>> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<IReadOnlyCollection<IGuild>>(ImmutableArray.Create<IGuild>());
/// <inheritdoc />
Task<IGuild> IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options)
=> throw new NotSupportedException();

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

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

/// <inheritdoc />
Task<IWebhook> IDiscordClient.GetWebhookAsync(ulong id, RequestOptions options)
=> Task.FromResult<IWebhook>(null);

/// <inheritdoc />
Task IDiscordClient.StartAsync()
=> Task.Delay(0);
/// <inheritdoc />
Task IDiscordClient.StopAsync()
=> Task.Delay(0);
}


+ 6
- 22
src/Discord.Net.Rest/DiscordRestClient.cs View File

@@ -33,65 +33,49 @@ namespace Discord.Rest
_applicationInfo = null;
return Task.Delay(0);
}

/// <inheritdoc />
public async Task<RestApplication> GetApplicationInfoAsync(RequestOptions options = null)
{
return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, options).ConfigureAwait(false));
}
/// <inheritdoc />
public Task<RestChannel> GetChannelAsync(ulong id, RequestOptions options = null)
=> ClientHelper.GetChannelAsync(this, id, options);
/// <inheritdoc />
public Task<IReadOnlyCollection<IRestPrivateChannel>> GetPrivateChannelsAsync(RequestOptions options = null)
=> ClientHelper.GetPrivateChannelsAsync(this, options);
public Task<IReadOnlyCollection<RestDMChannel>> GetDMChannelsAsync(RequestOptions options = null)
=> ClientHelper.GetDMChannelsAsync(this, options);
public Task<IReadOnlyCollection<RestGroupChannel>> GetGroupChannelsAsync(RequestOptions options = null)
=> ClientHelper.GetGroupChannelsAsync(this, options);

/// <inheritdoc />
public Task<IReadOnlyCollection<RestConnection>> GetConnectionsAsync(RequestOptions options = null)
=> ClientHelper.GetConnectionsAsync(this, options);

/// <inheritdoc />
public Task<RestInvite> GetInviteAsync(string inviteId, RequestOptions options = null)
=> ClientHelper.GetInviteAsync(this, inviteId, options);

/// <inheritdoc />
public Task<RestGuild> GetGuildAsync(ulong id, RequestOptions options = null)
=> ClientHelper.GetGuildAsync(this, id, options);
/// <inheritdoc />
public Task<RestGuildEmbed?> GetGuildEmbedAsync(ulong id, RequestOptions options = null)
=> ClientHelper.GetGuildEmbedAsync(this, id, options);
/// <inheritdoc />
public IAsyncEnumerable<IReadOnlyCollection<RestUserGuild>> GetGuildSummariesAsync(RequestOptions options = null)
=> ClientHelper.GetGuildSummariesAsync(this, null, null, options);
/// <inheritdoc />
public IAsyncEnumerable<IReadOnlyCollection<RestUserGuild>> GetGuildSummariesAsync(ulong fromGuildId, int limit, RequestOptions options = null)
=> ClientHelper.GetGuildSummariesAsync(this, fromGuildId, limit, options);
/// <inheritdoc />
public Task<IReadOnlyCollection<RestGuild>> GetGuildsAsync(RequestOptions options = null)
=> ClientHelper.GetGuildsAsync(this, options);
/// <inheritdoc />
public Task<RestGuild> CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null)
=> ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, options);

/// <inheritdoc />
public Task<RestUser> GetUserAsync(ulong id, RequestOptions options = null)
=> ClientHelper.GetUserAsync(this, id, options);
/// <inheritdoc />
public Task<RestGuildUser> GetGuildUserAsync(ulong guildId, ulong id, RequestOptions options = null)
=> ClientHelper.GetGuildUserAsync(this, guildId, id, options);

/// <inheritdoc />
public Task<IReadOnlyCollection<RestVoiceRegion>> GetVoiceRegionsAsync(RequestOptions options = null)
=> ClientHelper.GetVoiceRegionsAsync(this, options);
/// <inheritdoc />
public Task<RestVoiceRegion> GetVoiceRegionAsync(string id, RequestOptions options = null)
=> ClientHelper.GetVoiceRegionAsync(this, id, options);
/// <inheritdoc />
public Task<RestWebhook> GetWebhookAsync(ulong id, RequestOptions options = null)
=> ClientHelper.GetWebhookAsync(this, id, options);



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

@@ -1,7 +1,10 @@
using Discord.Net.Rest;
using Discord.Net.Rest;

namespace Discord.Rest
{
/// <summary>
/// Represents a configuration class for <see cref="DiscordRestClient" />.
/// </summary>
public class DiscordRestConfig : DiscordConfig
{
/// <summary> Gets or sets the provider used to generate new REST connections. </summary>


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

@@ -288,7 +288,7 @@ namespace Discord.Rest
}
public static IDisposable EnterTypingState(IMessageChannel channel, BaseDiscordClient client,
RequestOptions options)
=> new TypingNotifier(client, channel, options);
=> new TypingNotifier(channel, options);

//Webhooks
public static async Task<RestWebhook> CreateWebhookAsync(ITextChannel channel, BaseDiscordClient client, string name, Stream avatar, RequestOptions options)


+ 2
- 1
src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs View File

@@ -3,7 +3,7 @@ using Model = Discord.API.InviteMetadata;

namespace Discord.Rest
{
/// <summary> Represents additional information regarding the REST invite object. </summary>
/// <summary> Represents additional information regarding the REST-based invite object. </summary>
public class RestInviteMetadata : RestInvite, IInviteMetadata
{
private long _createdAtTicks;
@@ -48,6 +48,7 @@ namespace Discord.Rest
_createdAtTicks = model.CreatedAt.UtcTicks;
}

/// <inheritdoc />
IUser IInviteMetadata.Inviter => Inviter;
}
}

+ 2
- 1
src/Discord.Net.Rest/Net/Converters/ImageConverter.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using Newtonsoft.Json;
using Model = Discord.API.Image;
@@ -13,6 +13,7 @@ namespace Discord.Net.Converters
public override bool CanRead => true;
public override bool CanWrite => true;

/// <exception cref="InvalidOperationException">Cannot read from image.</exception>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new InvalidOperationException();


+ 5
- 3
src/Discord.Net.Rest/Net/Converters/PermissionTargetConverter.cs View File

@@ -1,4 +1,4 @@
using Newtonsoft.Json;
using Newtonsoft.Json;
using System;

namespace Discord.Net.Converters
@@ -11,6 +11,7 @@ namespace Discord.Net.Converters
public override bool CanRead => true;
public override bool CanWrite => true;

/// <exception cref="JsonSerializationException">Unknown permission target.</exception>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
switch ((string)reader.Value)
@@ -20,10 +21,11 @@ namespace Discord.Net.Converters
case "role":
return PermissionTarget.Role;
default:
throw new JsonSerializationException("Unknown permission target");
throw new JsonSerializationException("Unknown permission target.");
}
}

/// <exception cref="JsonSerializationException">Invalid permission target.</exception>
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
switch ((PermissionTarget)value)
@@ -35,7 +37,7 @@ namespace Discord.Net.Converters
writer.WriteValue("role");
break;
default:
throw new JsonSerializationException("Invalid permission target");
throw new JsonSerializationException("Invalid permission target.");
}
}
}


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

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
#if DEBUG_LIMITS
using System.Diagnostics;
@@ -117,7 +117,7 @@ namespace Discord.Net.Queue
if ((now - bucket.LastAttemptAt).TotalMinutes > 1.0)
_buckets.TryRemove(bucket.Id, out RequestBucket ignored);
}
await Task.Delay(60000, _cancelToken.Token); //Runs each minute
await Task.Delay(60000, _cancelToken.Token).ConfigureAwait(false); //Runs each minute
}
}
catch (OperationCanceledException) { }


+ 9
- 7
src/Discord.Net.Rest/Utils/TypingNotifier.cs View File

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

@@ -6,21 +6,19 @@ namespace Discord.Rest
{
internal class TypingNotifier : IDisposable
{
private readonly BaseDiscordClient _client;
private readonly CancellationTokenSource _cancelToken;
private readonly IMessageChannel _channel;
private readonly RequestOptions _options;

public TypingNotifier(BaseDiscordClient discord, IMessageChannel channel, RequestOptions options)
public TypingNotifier(IMessageChannel channel, RequestOptions options)
{
_client = discord;
_cancelToken = new CancellationTokenSource();
_channel = channel;
_options = options;
var _ = Run();
_ = RunAsync();
}

private async Task Run()
private async Task RunAsync()
{
try
{
@@ -31,7 +29,11 @@ namespace Discord.Rest
{
await _channel.TriggerTypingAsync(_options).ConfigureAwait(false);
}
catch { }
catch
{
// ignored
}

await Task.Delay(9500, token).ConfigureAwait(false);
}
}


+ 7
- 7
src/Discord.Net.WebSocket/Audio/AudioClient.cs View File

@@ -1,4 +1,4 @@
using Discord.API.Voice;
using Discord.API.Voice;
using Discord.Audio.Streams;
using Discord.Logging;
using Discord.Net.Converters;
@@ -65,7 +65,7 @@ namespace Discord.Audio

ApiClient = new DiscordVoiceAPIClient(guild.Id, Discord.WebSocketProvider, Discord.UdpSocketProvider);
ApiClient.SentGatewayMessage += async opCode => await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false);
ApiClient.SentDiscovery += async () => await _audioLogger.DebugAsync($"Sent Discovery").ConfigureAwait(false);
ApiClient.SentDiscovery += async () => await _audioLogger.DebugAsync("Sent Discovery").ConfigureAwait(false);
//ApiClient.SentData += async bytes => await _audioLogger.DebugAsync($"Sent {bytes} Bytes").ConfigureAwait(false);
ApiClient.ReceivedEvent += ProcessMessageAsync;
ApiClient.ReceivedPacket += ProcessPacketAsync;
@@ -291,7 +291,7 @@ namespace Discord.Audio
{
if (packet.Length != 70)
{
await _audioLogger.DebugAsync($"Malformed Packet").ConfigureAwait(false);
await _audioLogger.DebugAsync("Malformed Packet").ConfigureAwait(false);
return;
}
string ip;
@@ -303,7 +303,7 @@ namespace Discord.Audio
}
catch (Exception ex)
{
await _audioLogger.DebugAsync($"Malformed Packet", ex).ConfigureAwait(false);
await _audioLogger.DebugAsync("Malformed Packet", ex).ConfigureAwait(false);
return;
}
@@ -343,7 +343,7 @@ namespace Discord.Audio
{
if (!RTPReadStream.TryReadSsrc(packet, 0, out var ssrc))
{
await _audioLogger.DebugAsync($"Malformed Frame").ConfigureAwait(false);
await _audioLogger.DebugAsync("Malformed Frame").ConfigureAwait(false);
return;
}
if (!_ssrcMap.TryGetValue(ssrc, out var userId))
@@ -362,7 +362,7 @@ namespace Discord.Audio
}
catch (Exception ex)
{
await _audioLogger.DebugAsync($"Malformed Frame", ex).ConfigureAwait(false);
await _audioLogger.DebugAsync("Malformed Frame", ex).ConfigureAwait(false);
return;
}
//await _audioLogger.DebugAsync($"Received {packet.Length} bytes from user {userId}").ConfigureAwait(false);
@@ -371,7 +371,7 @@ namespace Discord.Audio
}
catch (Exception ex)
{
await _audioLogger.WarningAsync($"Failed to process UDP packet", ex).ConfigureAwait(false);
await _audioLogger.WarningAsync("Failed to process UDP packet", ex).ConfigureAwait(false);
return;
}
}


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

@@ -116,7 +116,7 @@ namespace Discord.Audio.Streams
timestamp += OpusEncoder.FrameSamplesPerChannel;
}
#if DEBUG
var _ = _logger?.DebugAsync($"Buffer underrun");
var _ = _logger?.DebugAsync("Buffer underrun");
#endif
}
}
@@ -140,7 +140,7 @@ namespace Discord.Audio.Streams
if (!_bufferPool.TryDequeue(out byte[] buffer))
{
#if DEBUG
var _ = _logger?.DebugAsync($"Buffer overflow"); //Should never happen because of the queueLock
var _ = _logger?.DebugAsync("Buffer overflow"); //Should never happen because of the queueLock
#endif
return;
}
@@ -149,7 +149,7 @@ namespace Discord.Audio.Streams
if (!_isPreloaded && _queuedFrames.Count == _queueLength)
{
#if DEBUG
var _ = _logger?.DebugAsync($"Preloaded");
var _ = _logger?.DebugAsync("Preloaded");
#endif
_isPreloaded = true;
}
@@ -173,4 +173,4 @@ namespace Discord.Audio.Streams
return Task.Delay(0);
}
}
}
}

+ 14
- 11
src/Discord.Net.WebSocket/BaseSocketClient.cs View File

@@ -26,18 +26,12 @@ namespace Discord.WebSocket
: base(config, client) => _baseconfig = config;
private static DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config)
=> new DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent);

/// <inheritdoc />
public abstract Task<RestApplication> GetApplicationInfoAsync(RequestOptions options = null);
/// <inheritdoc />
public abstract SocketUser GetUser(ulong id);
/// <inheritdoc />
public abstract SocketUser GetUser(string username, string discriminator);
/// <inheritdoc />
public abstract SocketChannel GetChannel(ulong id);
/// <inheritdoc />
public abstract SocketGuild GetGuild(ulong id);
/// <inheritdoc />
public abstract RestVoiceRegion GetVoiceRegion(string id);
/// <inheritdoc />
public abstract Task StartAsync();
@@ -47,47 +41,56 @@ namespace Discord.WebSocket
public abstract Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing);
public abstract Task SetActivityAsync(IActivity activity);
public abstract Task DownloadUsersAsync(IEnumerable<IGuild> guilds);

/// <inheritdoc />
public Task<RestGuild> CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null)
=> ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, options ?? RequestOptions.Default);
/// <inheritdoc />
public Task<IReadOnlyCollection<RestConnection>> GetConnectionsAsync(RequestOptions options = null)
=> ClientHelper.GetConnectionsAsync(this, options ?? RequestOptions.Default);
/// <inheritdoc />
public Task<RestInvite> GetInviteAsync(string inviteId, RequestOptions options = null)
=> ClientHelper.GetInviteAsync(this, inviteId, options ?? RequestOptions.Default);
// IDiscordClient
/// <inheritdoc />
async Task<IApplication> IDiscordClient.GetApplicationInfoAsync(RequestOptions options)
=> await GetApplicationInfoAsync(options).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 />
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 />
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, options).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<IVoiceRegion> IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options)
=> Task.FromResult<IVoiceRegion>(GetVoiceRegion(id));
/// <inheritdoc />
Task<IReadOnlyCollection<IVoiceRegion>> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options)
=> Task.FromResult<IReadOnlyCollection<IVoiceRegion>>(VoiceRegions);
}


+ 23
- 6
src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs View File

@@ -5,20 +5,37 @@ namespace Discord.Commands
/// <summary> The WebSocket variant of <see cref="ICommandContext"/>, which may contain the client, user, guild, channel, and message. </summary>
public class SocketCommandContext : ICommandContext
{
/// <summary> Gets the <see cref="DiscordSocketClient"/> that the command is executed with. </summary>
/// <summary>
/// Gets the <see cref="DiscordSocketClient" /> that the command is executed with.
/// </summary>
public DiscordSocketClient Client { get; }
/// <summary> Gets the <see cref="SocketGuild"/> that the command is executed in. </summary>
/// <summary>
/// Gets the <see cref="SocketGuild" /> that the command is executed in.
/// </summary>
public SocketGuild Guild { get; }
/// <summary> Gets the <see cref="ISocketMessageChannel"/> that the command is executed in. </summary>
/// <summary>
/// Gets the <see cref="ISocketMessageChannel" /> that the command is executed in.
/// </summary>
public ISocketMessageChannel Channel { get; }
/// <summary> Gets the <see cref="SocketUser"/> who executed the command. </summary>
/// <summary>
/// Gets the <see cref="SocketUser" /> who executed the command.
/// </summary>
public SocketUser User { get; }
/// <summary> Gets the <see cref="SocketUserMessage"/> that the command is interpreted from. </summary>
/// <summary>
/// Gets the <see cref="SocketUserMessage" /> that the command is interpreted from.
/// </summary>
public SocketUserMessage Message { get; }

/// <summary> Indicates whether the channel that the command is executed in is a private channel. </summary>
/// <summary>
/// Indicates whether the channel that the command is executed in is a private channel.
/// </summary>
public bool IsPrivate => Channel is IPrivateChannel;

/// <summary>
/// Initializes a new <see cref="SocketCommandContext" /> class with the provided client and message.
/// </summary>
/// <param name="client">The underlying client.</param>
/// <param name="msg">The underlying message.</param>
public SocketCommandContext(DiscordSocketClient client, SocketUserMessage msg)
{
Client = client;


+ 30
- 8
src/Discord.Net.WebSocket/DiscordShardedClient.cs View File

@@ -19,24 +19,29 @@ namespace Discord.WebSocket
private int _totalShards;
private bool _automaticShards;
/// <summary> Gets the estimated round-trip latency, in milliseconds, to the gateway server. </summary>
/// <inheritdoc />
public override int Latency { get => GetLatency(); protected set { } }
/// <inheritdoc />
public override UserStatus Status { get => _shards[0].Status; protected set { } }
/// <inheritdoc />
public override IActivity Activity { get => _shards[0].Activity; protected set { } }

internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient;
public override IReadOnlyCollection<SocketGuild> Guilds => GetGuilds().ToReadOnlyCollection(() => GetGuildCount());
public override IReadOnlyCollection<ISocketPrivateChannel> PrivateChannels => GetPrivateChannels().ToReadOnlyCollection(() => GetPrivateChannelCount());
/// <inheritdoc />
public override IReadOnlyCollection<SocketGuild> Guilds => GetGuilds().ToReadOnlyCollection(GetGuildCount);
/// <inheritdoc />
public override IReadOnlyCollection<ISocketPrivateChannel> PrivateChannels => GetPrivateChannels().ToReadOnlyCollection(GetPrivateChannelCount);
public IReadOnlyCollection<DiscordSocketClient> Shards => _shards;
/// <inheritdoc />
public override IReadOnlyCollection<RestVoiceRegion> VoiceRegions => _shards[0].VoiceRegions;

/// <summary> Creates a new REST/WebSocket discord client. </summary>
/// <summary> Creates a new REST/WebSocket Discord client. </summary>
public DiscordShardedClient() : this(null, new DiscordSocketConfig()) { }
/// <summary> Creates a new REST/WebSocket discord client. </summary>
/// <summary> Creates a new REST/WebSocket Discord client. </summary>
public DiscordShardedClient(DiscordSocketConfig config) : this(null, config, CreateApiClient(config)) { }
/// <summary> Creates a new REST/WebSocket discord client. </summary>
/// <summary> Creates a new REST/WebSocket Discord client. </summary>
public DiscordShardedClient(int[] ids) : this(ids, new DiscordSocketConfig()) { }
/// <summary> Creates a new REST/WebSocket discord client. </summary>
/// <summary> Creates a new REST/WebSocket Discord client. </summary>
public DiscordShardedClient(int[] ids, DiscordSocketConfig config) : this(ids, config, CreateApiClient(config)) { }
private DiscordShardedClient(int[] ids, DiscordSocketConfig config, API.DiscordSocketApiClient client)
: base(config, client)
@@ -213,7 +218,9 @@ namespace Discord.WebSocket
public override RestVoiceRegion GetVoiceRegion(string id)
=> _shards[0].GetVoiceRegion(id);

/// <summary> Downloads the users list for the provided guilds, if they don't have a complete list. </summary>
/// <summary>
/// Downloads the users list for the provided guilds if they don't have a complete list.
/// </summary>
public override async Task DownloadUsersAsync(IEnumerable<IGuild> guilds)
{
for (int i = 0; i < _shards.Length; i++)
@@ -233,11 +240,13 @@ namespace Discord.WebSocket
return (int)Math.Round(total / (double)_shards.Length);
}

/// <inheritdoc />
public override async Task SetStatusAsync(UserStatus status)
{
for (int i = 0; i < _shards.Length; i++)
await _shards[i].SetStatusAsync(status).ConfigureAwait(false);
}
/// <inheritdoc />
public override async Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing)
{
IActivity activity = null;
@@ -247,6 +256,7 @@ namespace Discord.WebSocket
activity = new Game(name, type);
await SetActivityAsync(activity).ConfigureAwait(false);
}
/// <inheritdoc />
public override async Task SetActivityAsync(IActivity activity)
{
for (int i = 0; i < _shards.Length; i++)
@@ -316,34 +326,46 @@ namespace Discord.WebSocket
}

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


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

@@ -508,7 +508,7 @@ namespace Discord.WebSocket
{
type = "GUILD_AVAILABLE";
_lastGuildAvailableTime = Environment.TickCount;
await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_AVAILABLE)").ConfigureAwait(false);
await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_AVAILABLE)").ConfigureAwait(false);

var guild = State.GetGuild(data.Id);
if (guild != null)
@@ -533,7 +533,7 @@ namespace Discord.WebSocket
}
else
{
await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_CREATE)").ConfigureAwait(false);
await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_CREATE)").ConfigureAwait(false);

var guild = AddGuild(data, State);
if (guild != null)
@@ -614,7 +614,7 @@ namespace Discord.WebSocket
if (data.Unavailable == true)
{
type = "GUILD_UNAVAILABLE";
await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_UNAVAILABLE)").ConfigureAwait(false);
await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_UNAVAILABLE)").ConfigureAwait(false);

var guild = State.GetGuild(data.Id);
if (guild != null)
@@ -630,7 +630,7 @@ namespace Discord.WebSocket
}
else
{
await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_DELETE)").ConfigureAwait(false);
await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_DELETE)").ConfigureAwait(false);

var guild = RemoveGuild(data.Id);
if (guild != null)


+ 43
- 12
src/Discord.Net.WebSocket/DiscordSocketConfig.cs View File

@@ -1,41 +1,72 @@
using Discord.Net.Udp;
using Discord.Net.Udp;
using Discord.Net.WebSockets;
using Discord.Rest;

namespace Discord.WebSocket
{
/// <summary>
/// Represents a configuration class for <see cref="DiscordSocketClient" />.
/// </summary>
public class DiscordSocketConfig : DiscordRestConfig
{
/// <summary>
/// Gets or sets the encoding gateway should use.
/// </summary>
public const string GatewayEncoding = "json";

/// <summary> Gets or sets the websocket host to connect to. If null, the client will use the /gateway endpoint. </summary>
/// <summary>
/// Gets or sets the WebSocket host to connect to. If <see langword="null"/>, the client will use the
/// /gateway endpoint.
/// </summary>
public string GatewayHost { get; set; } = null;

/// <summary> Gets or sets the time, in milliseconds, to wait for a connection to complete before aborting. </summary>
/// <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 id for this shard. Must be less than TotalShards. </summary>
/// <summary>
/// Gets or sets the ID for this shard. Must be less than <see cref="TotalShards"/>.
/// </summary>
public int? ShardId { get; set; } = null;
/// <summary> Gets or sets the total number of shards for this application. </summary>
/// <summary>
/// Gets or sets the total number of shards for this application.
/// </summary>
public int? TotalShards { get; set; } = null;

/// <summary> Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero disables the message cache entirely. </summary>
/// <summary>
/// Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero
/// disables the message cache entirely.
/// </summary>
public int MessageCacheSize { get; set; } = 0;
/// <summary>
/// Gets or sets the max number of users a guild may have for offline users to be included in the READY packet. Max is 250.
/// <summary>
/// Gets or sets the max number of users a guild may have for offline users to be included in the READY
/// packet. Max is 250.
/// </summary>
public int LargeThreshold { get; set; } = 250;

/// <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; }
/// <summary> Gets or sets the provider used to generate new udp sockets. </summary>
/// <summary>
/// Gets or sets the provider used to generate new UDP sockets.
/// </summary>
public UdpSocketProvider UdpSocketProvider { get; set; }

/// <summary> Gets or sets whether or not all users should be downloaded as guilds come available. </summary>
/// <summary>
/// Gets or sets whether or not all users should be downloaded as guilds come available.
/// </summary>
public bool AlwaysDownloadUsers { get; set; } = false;
/// <summary> Gets or sets the timeout for event handlers, in milliseconds, after which a warning will be logged. Null disables this check. </summary>
/// <summary>
/// Gets or sets the timeout for event handlers, in milliseconds, after which a warning will be logged. Null
/// disables this check.
/// </summary>
public int? HandlerTimeout { get; set; } = 3000;

/// <summary>
/// Initializes a default configuration.
/// </summary>
public DiscordSocketConfig()
{
WebSocketProvider = DefaultWebSocketProvider.Instance;


+ 4
- 1
src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs View File

@@ -1,5 +1,8 @@
namespace Discord.WebSocket
namespace Discord.WebSocket
{
/// <summary>
/// Represents a generic WebSocket-based audio channel.
/// </summary>
public interface ISocketAudioChannel : IAudioChannel
{
}


+ 44
- 4
src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs View File

@@ -5,18 +5,52 @@ using System.Threading.Tasks;

namespace Discord.WebSocket
{
/// <summary>
/// Represents a generic WebSocket-based channel that can send and receive messages.
/// </summary>
public interface ISocketMessageChannel : IMessageChannel
{
/// <summary> Gets all messages in this channel's cache. </summary>
IReadOnlyCollection<SocketMessage> CachedMessages { get; }

/// <summary> Sends a message to this message channel. </summary>
/// <summary>
/// Sends a message to this message channel.
/// </summary>
/// <param name="text">The message to be sent.</param>
/// <param name="isTTS">Whether the message should be read aloud by Discord or not.</param>
/// <param name="embed">The <see cref="EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param>
new Task<RestUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null);
#if FILESYSTEM
/// <summary> Sends a file to this text channel, with an optional caption. </summary>
/// <summary>
/// Sends a file to this message channel, with an optional caption.
/// </summary>
/// <param name="filePath">The file path of the file.</param>
/// <param name="text">The message to be sent.</param>
/// <param name="isTTS">Whether the message should be read aloud by Discord or not.</param>
/// <param name="embed">The <see cref="EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <remarks>
/// If you wish to upload an image and have it embedded in a <see cref="EmbedType.Rich"/> embed, you may
/// upload the file and refer to the file with "attachment://filename.ext" in the
/// <see cref="Discord.EmbedBuilder.ImageUrl"/>.
/// </remarks>
new Task<RestUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null);
#endif
/// <summary> Sends a file to this text channel, with an optional caption. </summary>
/// <summary>
/// Sends a file to this message channel, with an optional caption.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> of the file to be sent.</param>
/// <param name="filename">The name of the attachment.</param>
/// <param name="text">The message to be sent.</param>
/// <param name="isTTS">Whether the message should be read aloud by Discord or not.</param>
/// <param name="embed">The <see cref="EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <remarks>
/// If you wish to upload an image and have it embedded in a <see cref="EmbedType.Rich"/> embed, you may
/// upload the file and refer to the file with "attachment://filename.ext" in the
/// <see cref="Discord.EmbedBuilder.ImageUrl"/>.
/// </remarks>
new Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null);

SocketMessage GetCachedMessage(ulong id);
@@ -26,7 +60,13 @@ namespace Discord.WebSocket
IReadOnlyCollection<SocketMessage> GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch);
/// <summary> Gets a collection of messages in this channel. </summary>
IReadOnlyCollection<SocketMessage> GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch);
/// <summary> Gets a collection of pinned messages in this channel. </summary>
/// <summary>
/// Gets a collection of pinned messages in this channel.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A collection of messages.
/// </returns>
new Task<IReadOnlyCollection<RestMessage>> GetPinnedMessagesAsync(RequestOptions options = null);
}
}

+ 4
- 1
src/Discord.Net.WebSocket/Entities/Channels/ISocketPrivateChannel.cs View File

@@ -1,7 +1,10 @@
using System.Collections.Generic;
using System.Collections.Generic;

namespace Discord.WebSocket
{
/// <summary>
/// Represents a generic WebSocket-based channel that is private to select recipients.
/// </summary>
public interface ISocketPrivateChannel : IPrivateChannel
{
new IReadOnlyCollection<SocketUser> Recipients { get; }


+ 3
- 3
src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs View File

@@ -3,14 +3,14 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Discord.Audio;
using Discord.Rest;
using Model = Discord.API.Channel;

namespace Discord.WebSocket
{
/// <summary>
/// Represents a WebSocket-based category channel.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketCategoryChannel : SocketGuildChannel, ICategoryChannel
{


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

@@ -1,4 +1,3 @@
using Discord.Rest;
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -8,16 +7,27 @@ using Model = Discord.API.Channel;

namespace Discord.WebSocket
{
/// <summary>
/// Represents a WebSocket-based channel.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public abstract class SocketChannel : SocketEntity<ulong>, IChannel
{
/// <summary>
/// Gets when the channel is created.
/// </summary>
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);
/// <summary>
/// Gets a collection of users from the WebSocket cache.
/// </summary>
public IReadOnlyCollection<SocketUser> Users => GetUsersInternal();

internal SocketChannel(DiscordSocketClient discord, ulong id)
: base(discord, id)
{
}

/// <exception cref="InvalidOperationException">Unexpected channel type is created.</exception>
internal static ISocketPrivateChannel CreatePrivate(DiscordSocketClient discord, ClientState state, Model model)
{
switch (model.Type)
@@ -33,6 +43,17 @@ namespace Discord.WebSocket
internal abstract void Update(ClientState state, Model model);

//User
/// <summary>
/// Gets the user from the WebSocket cache.
/// </summary>
/// <remarks>
/// This method does NOT attempt to fetch the user if they don't exist in the cache. To guarantee a return
/// from an existing user that doesn't exist in cache, use <see cref="Rest.DiscordRestClient.GetUserAsync" />.
/// </remarks>
/// <param name="id">The ID of the user.</param>
/// <returns>
/// The user.
/// </returns>
public SocketUser GetUser(ulong id) => GetUserInternal(id);
internal abstract SocketUser GetUserInternal(ulong id);
internal abstract IReadOnlyCollection<SocketUser> GetUsersInternal();
@@ -40,10 +61,13 @@ namespace Discord.WebSocket
internal SocketChannel Clone() => MemberwiseClone() as SocketChannel;

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


+ 38
- 3
src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs View File

@@ -10,13 +10,17 @@ using Model = Discord.API.Channel;

namespace Discord.WebSocket
{
/// <summary>
/// Represents a WebSocket-based direct-message channel.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketDMChannel : SocketChannel, IDMChannel, ISocketPrivateChannel, ISocketMessageChannel
{
private readonly MessageCache _messages;

public SocketUser Recipient { get; private set; }
public SocketUser Recipient { get; }

/// <inheritdoc />
public IReadOnlyCollection<SocketMessage> CachedMessages => _messages?.Messages ?? ImmutableArray.Create<SocketMessage>();
public new IReadOnlyCollection<SocketUser> Users => ImmutableArray.Create(Discord.CurrentUser, Recipient);

@@ -39,10 +43,12 @@ namespace Discord.WebSocket
Recipient.Update(state, model.Recipients.Value[0]);
}

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

//Messages
/// <inheritdoc />
public SocketMessage GetCachedMessage(ulong id)
=> _messages?.Get(id);
public async Task<IMessage> GetMessageAsync(ulong id, RequestOptions options = null)
@@ -58,26 +64,35 @@ namespace Discord.WebSocket
=> SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, options);
public IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null)
=> SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, options);
/// <inheritdoc />
public IReadOnlyCollection<SocketMessage> GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch)
=> SocketChannelHelper.GetCachedMessages(this, Discord, _messages, null, Direction.Before, limit);
/// <inheritdoc />
public IReadOnlyCollection<SocketMessage> GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch)
=> SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessageId, dir, limit);
/// <inheritdoc />
public IReadOnlyCollection<SocketMessage> GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch)
=> SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessage.Id, dir, limit);
/// <inheritdoc />
public Task<IReadOnlyCollection<RestMessage>> GetPinnedMessagesAsync(RequestOptions options = null)
=> ChannelHelper.GetPinnedMessagesAsync(this, Discord, options);

/// <inheritdoc />
public Task<RestUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options);
#if FILESYSTEM
/// <inheritdoc />
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options);
#endif
/// <inheritdoc />
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options);

/// <inheritdoc />
public Task TriggerTypingAsync(RequestOptions options = null)
=> ChannelHelper.TriggerTypingAsync(this, Discord, options);
/// <inheritdoc />
public IDisposable EnterTypingState(RequestOptions options = null)
=> ChannelHelper.EnterTypingState(this, Discord, options);

@@ -97,24 +112,33 @@ namespace Discord.WebSocket
return null;
}

/// <summary>
/// Returns the recipient user.
/// </summary>
public override string ToString() => $"@{Recipient}";
private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)";
internal new SocketDMChannel Clone() => MemberwiseClone() as SocketDMChannel;

//SocketChannel
/// <inheritdoc />
internal override IReadOnlyCollection<SocketUser> GetUsersInternal() => Users;
/// <inheritdoc />
internal override SocketUser GetUserInternal(ulong id) => GetUser(id);

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

//ISocketPrivateChannel
/// <inheritdoc />
IReadOnlyCollection<SocketUser> ISocketPrivateChannel.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)
@@ -122,30 +146,41 @@ namespace Discord.WebSocket
else
return GetCachedMessage(id);
}
/// <inheritdoc />
IAsyncEnumerable<IReadOnlyCollection<IMessage>> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options)
=> SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, mode, options);
/// <inheritdoc />
IAsyncEnumerable<IReadOnlyCollection<IMessage>> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options)
=> SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, mode, options);
/// <inheritdoc />
IAsyncEnumerable<IReadOnlyCollection<IMessage>> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options)
=> SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, options);
/// <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
//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();
}


+ 46
- 2
src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs View File

@@ -15,7 +15,7 @@ using VoiceStateModel = Discord.API.VoiceState;
namespace Discord.WebSocket
{
/// <summary>
/// Represents a private WebSocket group channel.
/// Represents a WebSocket-based private group channel.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketGroupChannel : SocketChannel, IGroupChannel, ISocketPrivateChannel, ISocketMessageChannel, ISocketAudioChannel
@@ -24,10 +24,12 @@ namespace Discord.WebSocket

private string _iconId;
private ConcurrentDictionary<ulong, SocketGroupUser> _users;
private ConcurrentDictionary<ulong, SocketVoiceState> _voiceStates;
private readonly ConcurrentDictionary<ulong, SocketVoiceState> _voiceStates;

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

/// <inheritdoc />
public IReadOnlyCollection<SocketMessage> CachedMessages => _messages?.Messages ?? ImmutableArray.Create<SocketMessage>();
public new IReadOnlyCollection<SocketGroupUser> Users => _users.ToReadOnlyCollection();
public IReadOnlyCollection<SocketGroupUser> Recipients
@@ -65,15 +67,18 @@ namespace Discord.WebSocket
_users = users;
}

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

/// <exception cref="NotSupportedException">Voice is not yet supported for group channels.</exception>
public Task<IAudioClient> ConnectAsync()
{
throw new NotSupportedException("Voice is not yet supported for group channels.");
}

//Messages
/// <inheritdoc />
public SocketMessage GetCachedMessage(ulong id)
=> _messages?.Get(id);
public async Task<IMessage> GetMessageAsync(ulong id, RequestOptions options = null)
@@ -89,26 +94,35 @@ namespace Discord.WebSocket
=> SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, options);
public IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null)
=> SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, options);
/// <inheritdoc />
public IReadOnlyCollection<SocketMessage> GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch)
=> SocketChannelHelper.GetCachedMessages(this, Discord, _messages, null, Direction.Before, limit);
/// <inheritdoc />
public IReadOnlyCollection<SocketMessage> GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch)
=> SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessageId, dir, limit);
/// <inheritdoc />
public IReadOnlyCollection<SocketMessage> GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch)
=> SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessage.Id, dir, limit);
/// <inheritdoc />
public Task<IReadOnlyCollection<RestMessage>> GetPinnedMessagesAsync(RequestOptions options = null)
=> ChannelHelper.GetPinnedMessagesAsync(this, Discord, options);

/// <inheritdoc />
public Task<RestUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options);
#if FILESYSTEM
/// <inheritdoc />
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options);
#endif
/// <inheritdoc />
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options);

/// <inheritdoc />
public Task TriggerTypingAsync(RequestOptions options = null)
=> ChannelHelper.TriggerTypingAsync(this, Discord, options);
/// <inheritdoc />
public IDisposable EnterTypingState(RequestOptions options = null)
=> ChannelHelper.EnterTypingState(this, Discord, options);

@@ -118,6 +132,17 @@ namespace Discord.WebSocket
=> _messages?.Remove(id);

//Users
/// <summary>
/// Gets the group user from the WebSocket cache.
/// </summary>
/// <remarks>
/// This method does NOT attempt to fetch the user if they don't exist in the cache. To guarantee a return
/// from an existing user that doesn't exist in cache, use <see cref="DiscordRestClient.GetUserAsync" />.
/// </remarks>
/// <param name="id">The ID of the user.</param>
/// <returns>
/// The user in the group.
/// </returns>
public new SocketGroupUser GetUser(ulong id)
{
if (_users.TryGetValue(id, out SocketGroupUser user))
@@ -167,21 +192,29 @@ namespace Discord.WebSocket
return null;
}

/// <summary>
/// Returns the name of the group.
/// </summary>
public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Id}, Group)";
internal new SocketGroupChannel Clone() => MemberwiseClone() as SocketGroupChannel;

//SocketChannel
/// <inheritdoc />
internal override IReadOnlyCollection<SocketUser> GetUsersInternal() => Users;
/// <inheritdoc />
internal override SocketUser GetUserInternal(ulong id) => GetUser(id);

//ISocketPrivateChannel
/// <inheritdoc />
IReadOnlyCollection<SocketUser> ISocketPrivateChannel.Recipients => Recipients;

//IPrivateChannel
/// <inheritdoc />
IReadOnlyCollection<IUser> IPrivateChannel.Recipients => Recipients;

//IMessageChannel
/// <inheritdoc />
async Task<IMessage> IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -189,31 +222,42 @@ namespace Discord.WebSocket
else
return GetCachedMessage(id);
}
/// <inheritdoc />
IAsyncEnumerable<IReadOnlyCollection<IMessage>> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options)
=> SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, mode, options);
/// <inheritdoc />
IAsyncEnumerable<IReadOnlyCollection<IMessage>> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options)
=> SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, mode, options);
/// <inheritdoc />
IAsyncEnumerable<IReadOnlyCollection<IMessage>> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options)
=> SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, options);
/// <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);

//IAudioChannel
/// <inheritdoc />
Task<IAudioClient> IAudioChannel.ConnectAsync(Action<IAudioClient> configAction) { throw new NotSupportedException(); }

//IChannel
/// <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();
}


+ 25
- 1
src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs View File

@@ -9,12 +9,20 @@ using Model = Discord.API.Channel;

namespace Discord.WebSocket
{
/// <summary> The WebSocket variant of <see cref="IGuildChannel"/>. Represents a guild channel (text, voice, category). </summary>
/// <summary>
/// Represents a WebSocket-based guild channel.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketGuildChannel : SocketChannel, IGuildChannel
{
private ImmutableArray<Overwrite> _overwrites;

/// <summary>
/// Gets the guild associated with this channel.
/// </summary>
/// <returns>
/// The guild that this channel belongs to.
/// </returns>
public SocketGuild Guild { get; }
/// <inheritdoc />
public string Name { get; private set; }
@@ -22,11 +30,23 @@ namespace Discord.WebSocket
public int Position { get; private set; }
/// <inheritdoc />
public ulong? CategoryId { get; private set; }
/// <summary>
/// Gets the parent category of this channel.
/// </summary>
/// <returns>
/// The parent category ID associated with this channel, or <see langword="null"/> if none is set.
/// </returns>
public ICategoryChannel Category
=> CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null;

/// <inheritdoc />
public IReadOnlyCollection<Overwrite> PermissionOverwrites => _overwrites;
/// <summary>
/// Returns a collection of users that are able to view the channel.
/// </summary>
/// <returns>
/// A collection of users that can access the channel (i.e. the users seen in the user list).
/// </returns>
public new virtual IReadOnlyCollection<SocketGuildUser> Users => ImmutableArray.Create<SocketGuildUser>();

internal SocketGuildChannel(DiscordSocketClient discord, ulong id, SocketGuild guild)
@@ -131,7 +151,11 @@ namespace Discord.WebSocket

public new virtual SocketGuildUser GetUser(ulong id) => null;

/// <summary>
/// Gets the name of the channel.
/// </summary>
public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Id}, Guild)";
internal new SocketGuildChannel Clone() => MemberwiseClone() as SocketGuildChannel;

//SocketChannel


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

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

namespace Discord.WebSocket
{
/// <summary>
/// Represents a WebSocket-based channel in a guild that can send and receive messages.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketTextChannel : SocketGuildChannel, ITextChannel, ISocketMessageChannel
{
@@ -73,12 +76,16 @@ namespace Discord.WebSocket
=> SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, options);
public IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null)
=> SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, options);
/// <inheritdoc />
public IReadOnlyCollection<SocketMessage> GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch)
=> SocketChannelHelper.GetCachedMessages(this, Discord, _messages, null, Direction.Before, limit);
/// <inheritdoc />
public IReadOnlyCollection<SocketMessage> GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch)
=> SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessageId, dir, limit);
/// <inheritdoc />
public IReadOnlyCollection<SocketMessage> GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch)
=> SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessage.Id, dir, limit);
/// <inheritdoc />
public Task<IReadOnlyCollection<RestMessage>> GetPinnedMessagesAsync(RequestOptions options = null)
=> ChannelHelper.GetPinnedMessagesAsync(this, Discord, options);

@@ -104,6 +111,7 @@ 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);

@@ -128,10 +136,34 @@ namespace Discord.WebSocket
}

//Webhooks
/// <summary>
/// Creates a webhook in this text channel.
/// </summary>
/// <param name="name">The name of the webhook.</param>
/// <param name="avatar">The avatar of the webhook.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// The created webhook.
/// </returns>
public Task<RestWebhook> CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null)
=> ChannelHelper.CreateWebhookAsync(this, Discord, name, avatar, options);
/// <summary>
/// Gets the webhook in this text channel with the provided ID.
/// </summary>
/// <param name="id">The ID of the webhook.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A webhook associated with the <paramref name="id"/>, or <see langword="null"/> if not found.
/// </returns>
public Task<RestWebhook> GetWebhookAsync(ulong id, RequestOptions options = null)
=> ChannelHelper.GetWebhookAsync(this, Discord, id, options);
/// <summary>
/// Gets the webhooks for this text channel.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A collection of webhooks.
/// </returns>
public Task<IReadOnlyCollection<RestWebhook>> GetWebhooksAsync(RequestOptions options = null)
=> ChannelHelper.GetWebhooksAsync(this, Discord, options);



+ 12
- 1
src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs View File

@@ -1,4 +1,4 @@
using Discord.Audio;
using Discord.Audio;
using Discord.Rest;
using System;
using System.Collections.Generic;
@@ -10,12 +10,18 @@ using Model = Discord.API.Channel;

namespace Discord.WebSocket
{
/// <summary>
/// Represents a WebSocket-based voice channel in a guild.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketVoiceChannel : SocketGuildChannel, IVoiceChannel, ISocketAudioChannel
{
/// <inheritdoc />
public int Bitrate { get; private set; }
/// <inheritdoc />
public int? UserLimit { get; private set; }

/// <inheritdoc />
public override IReadOnlyCollection<SocketGuildUser> Users
=> Guild.Users.Where(x => x.VoiceChannel?.Id == Id).ToImmutableArray();

@@ -37,14 +43,17 @@ namespace Discord.WebSocket
UserLimit = model.UserLimit.Value != 0 ? model.UserLimit.Value : (int?)null;
}

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

/// <inheritdoc />
public async Task<IAudioClient> ConnectAsync(Action<IAudioClient> configAction = null)
{
return await Guild.ConnectAudioAsync(Id, false, false, configAction).ConfigureAwait(false);
}

/// <inheritdoc />
public override SocketGuildUser GetUser(ulong id)
{
var user = Guild.GetUser(id);
@@ -57,8 +66,10 @@ namespace Discord.WebSocket
internal new SocketVoiceChannel Clone() => MemberwiseClone() as SocketVoiceChannel;

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


+ 173
- 7
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs View File

@@ -21,6 +21,10 @@ using VoiceStateModel = Discord.API.VoiceState;

namespace Discord.WebSocket
{
/// <summary>
/// Represents a WebSocket-based guild object.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketGuild : SocketEntity<ulong>, IGuild
{
private readonly SemaphoreSlim _audioLock;
@@ -46,11 +50,13 @@ namespace Discord.WebSocket
public MfaLevel MfaLevel { get; private set; }
/// <inheritdoc />
public DefaultMessageNotifications DefaultMessageNotifications { get; private set; }
/// <summary> Gets the number of members. </summary>
/// <remark>
/// The number of members is returned by Discord and is the most accurate.
/// You may see discrepancy between the Users collection and this.
/// </remark>
/// <summary>
/// Gets the number of members.
/// </summary>
/// <remarks>
/// The number of members is returned by Discord and is the most accurate. You may see discrepancy between
/// the <see cref="Users"/> collection and this.
/// </remarks>
public int MemberCount { get; internal set; }
/// <summary> Gets the number of members downloaded to the local guild cache. </summary>
public int DownloadedMemberCount { get; private set; }
@@ -84,11 +90,23 @@ namespace Discord.WebSocket
public bool IsSynced => _syncPromise.Task.IsCompleted;
public Task SyncPromise => _syncPromise.Task;
public Task DownloaderPromise => _downloaderPromise.Task;
/// <summary>
/// Returns the <see cref="IAudioClient" /> associated with this guild.
/// </summary>
public IAudioClient AudioClient => _audioClient;
/// <summary>
/// Returns the first viewable text channel.
/// </summary>
/// <remarks>
/// This property does not guarantee the user can send message to it.
/// </remarks>
public SocketTextChannel DefaultChannel => TextChannels
.Where(c => CurrentUser.GetPermissions(c).ViewChannel)
.OrderBy(c => c.Position)
.FirstOrDefault();
/// <summary>
/// Returns the AFK voice channel, or <see langword="null" /> if none is set.
/// </summary>
public SocketVoiceChannel AFKChannel
{
get
@@ -97,6 +115,9 @@ namespace Discord.WebSocket
return id.HasValue ? GetVoiceChannel(id.Value) : null;
}
}
/// <summary>
/// Gets the embed channel set in the widget settings of this guild, or <see langword="null"/> if none is set.
/// </summary>
public SocketGuildChannel EmbedChannel
{
get
@@ -105,6 +126,9 @@ namespace Discord.WebSocket
return id.HasValue ? GetChannel(id.Value) : null;
}
}
/// <summary>
/// Gets the channel where randomized welcome messages are sent, or <see langword="null"/> if none is set.
/// </summary>
public SocketTextChannel SystemChannel
{
get
@@ -113,14 +137,32 @@ namespace Discord.WebSocket
return id.HasValue ? GetTextChannel(id.Value) : null;
}
}
/// <summary>
/// Returns a collection of text channels present in this guild.
/// </summary>
public IReadOnlyCollection<SocketTextChannel> TextChannels
=> Channels.Select(x => x as SocketTextChannel).Where(x => x != null).ToImmutableArray();
/// <summary>
/// Returns a collection of voice channels present in this guild.
/// </summary>
public IReadOnlyCollection<SocketVoiceChannel> VoiceChannels
=> Channels.Select(x => x as SocketVoiceChannel).Where(x => x != null).ToImmutableArray();
/// <summary>
/// Returns a collection of category channels present in this guild.
/// </summary>
public IReadOnlyCollection<SocketCategoryChannel> CategoryChannels
=> Channels.Select(x => x as SocketCategoryChannel).Where(x => x != null).ToImmutableArray();
/// <summary>
/// Returns the current logged-in user.
/// </summary>
public SocketGuildUser CurrentUser => _members.TryGetValue(Discord.CurrentUser.Id, out SocketGuildUser member) ? member : null;
/// <summary>
/// Returns the @everyone role in this guild.
/// </summary>
public SocketRole EveryoneRole => GetRole(Id);
/// <summary>
/// Returns a collection of channels present in this guild.
/// </summary>
public IReadOnlyCollection<SocketGuildChannel> Channels
{
get
@@ -130,9 +172,26 @@ namespace Discord.WebSocket
return channels.Select(x => state.GetChannel(x) as SocketGuildChannel).Where(x => x != null).ToReadOnlyCollection(channels);
}
}
/// <summary>
/// Gets a collection of emotes created in this guild.
/// </summary>
public IReadOnlyCollection<GuildEmote> Emotes => _emotes;
/// <summary>
/// Gets a collection of features enabled in this guild.
/// </summary>
public IReadOnlyCollection<string> Features => _features;
/// <summary>
/// Gets a collection of users in this guild.
/// </summary>
/// <remarks>
/// This property may not always return all the members for large guilds (i.e. guilds containing 100+ users).
/// You may need to enable <see cref="DiscordSocketConfig.AlwaysDownloadUsers"/> to fetch the full user list
/// upon startup, or use <see cref="DownloadUsersAsync"/> to manually download the users.
/// </remarks>
public IReadOnlyCollection<SocketGuildUser> Users => _members.ToReadOnlyCollection();
/// <summary>
/// Gets a collection of roles in this guild.
/// </summary>
public IReadOnlyCollection<SocketRole> Roles => _roles.ToReadOnlyCollection();

internal SocketGuild(DiscordSocketClient client, ulong id)
@@ -315,7 +374,13 @@ namespace Discord.WebSocket
=> GuildHelper.LeaveAsync(this, Discord, options);

//Bans
/// <summary> Gets a collection of the banned users in this guild. </summary>
/// <summary>
/// Gets a collection of the banned users in this guild.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A collection of bans.
/// </returns>
public Task<IReadOnlyCollection<RestBan>> GetBansAsync(RequestOptions options = null)
=> GuildHelper.GetBansAsync(this, Discord, options);

@@ -334,6 +399,13 @@ namespace Discord.WebSocket
=> GuildHelper.RemoveBanAsync(this, Discord, userId, options);

//Channels
/// <summary>
/// Returns a guild channel with the provided ID.
/// </summary>
/// <param name="id">The channel ID.</param>
/// <returns>
/// The guild channel associated with the ID.
/// </returns>
public SocketGuildChannel GetChannel(ulong id)
{
var channel = Discord.State.GetChannel(id) as SocketGuildChannel;
@@ -341,14 +413,52 @@ namespace Discord.WebSocket
return channel;
return null;
}
/// <summary>
/// Returns a text channel with the provided ID.
/// </summary>
/// <param name="id">The channel ID.</param>
/// <returns>
/// The text channel associated with the ID.
/// </returns>
public SocketTextChannel GetTextChannel(ulong id)
=> GetChannel(id) as SocketTextChannel;
/// <summary>
/// Returns a voice channel with the provided ID.
/// </summary>
/// <param name="id">The channel ID.</param>
/// <returns>
/// The voice channel associated with the ID.
/// </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>
/// <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>
/// <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>
/// <returns>
/// The created category channel.
/// </returns>
public Task<RestCategoryChannel> CreateCategoryChannelAsync(string name, RequestOptions options = null)
=> GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options);

@@ -373,16 +483,43 @@ namespace Discord.WebSocket
=> GuildHelper.CreateIntegrationAsync(this, Discord, id, type, options);

//Invites
/// <summary>
/// Returns a collection of invites associated with this channel.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A collection of invites.
/// </returns>
public Task<IReadOnlyCollection<RestInviteMetadata>> GetInvitesAsync(RequestOptions options = null)
=> GuildHelper.GetInvitesAsync(this, Discord, options);

//Roles
/// <summary>
/// Returns a role with the provided role ID.
/// </summary>
/// <param name="id">The ID of the role.</param>
/// <returns>
/// The role associated with the ID.
/// </returns>
public SocketRole GetRole(ulong id)
{
if (_roles.TryGetValue(id, out SocketRole value))
return value;
return null;
}
/// <summary>
/// Creates a role.
/// </summary>
/// <param name="name">The name of the new role.</param>
/// <param name="permissions">
/// The permissions that the new role possesses. Set to <see langword="null" /> to use the default permissions.
/// </param>
/// <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>
/// <returns>
/// The created role.
/// </returns>
public Task<RestRole> CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?),
bool isHoisted = false, RequestOptions options = null)
=> GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, options);
@@ -400,12 +537,20 @@ namespace Discord.WebSocket
}

//Users
/// <summary>
/// Gets the user with the provided ID.
/// </summary>
/// <param name="id">The ID of the user.</param>
/// <returns>
/// The user associated with the ID.
/// </returns>
public SocketGuildUser GetUser(ulong id)
{
if (_members.TryGetValue(id, out SocketGuildUser member))
return member;
return null;
}
/// <inheritdoc />
public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null)
=> GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options);

@@ -459,6 +604,9 @@ namespace Discord.WebSocket
return null;
}

/// <summary>
/// Downloads the users of this guild to the WebSocket cache.
/// </summary>
public async Task DownloadUsersAsync()
{
await Discord.DownloadUsersAsync(new[] { this }).ConfigureAwait(false);
@@ -469,8 +617,23 @@ namespace Discord.WebSocket
}

//Webhooks
/// <summary>
/// Returns the webhook with the provided ID.
/// </summary>
/// <param name="id">The ID of the webhook.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A webhook associated with the ID.
/// </returns>
public Task<RestWebhook> GetWebhookAsync(ulong id, RequestOptions options = null)
=> GuildHelper.GetWebhookAsync(this, Discord, id, options);
/// <summary>
/// Gets a collection of webhooks that exist in the guild.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A collection of webhooks.
/// </returns>
public Task<IReadOnlyCollection<RestWebhook>> GetWebhooksAsync(RequestOptions options = null)
=> GuildHelper.GetWebhooksAsync(this, Discord, options);

@@ -592,7 +755,7 @@ namespace Discord.WebSocket
try
{
var timeoutTask = Task.Delay(15000);
if (await Task.WhenAny(promise.Task, timeoutTask) == timeoutTask)
if (await Task.WhenAny(promise.Task, timeoutTask).ConfigureAwait(false) == timeoutTask)
throw new TimeoutException();
return await promise.Task.ConfigureAwait(false);
}
@@ -663,6 +826,9 @@ namespace Discord.WebSocket
}
}

/// <summary>
/// Gets the name of the guild.
/// </summary>
public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Id})";
internal SocketGuild Clone() => MemberwiseClone() as SocketGuild;


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

@@ -12,7 +12,9 @@ using PresenceModel = Discord.API.Presence;

namespace Discord.WebSocket
{
/// <summary> The WebSocket variant of <see cref="IGuildUser"/>. Represents a Discord user that is in a guild. </summary>
/// <summary>
/// Represents a WebSocket guild user.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketGuildUser : SocketUser, IGuildUser
{
@@ -20,6 +22,9 @@ namespace Discord.WebSocket
private ImmutableArray<ulong> _roleIds;

internal override SocketGlobalUser GlobalUser { get; }
/// <summary>
/// Gets the guild the user is in.
/// </summary>
public SocketGuild Guild { get; }
/// <inheritdoc />
public string Nickname { get; private set; }
@@ -50,17 +55,27 @@ namespace Discord.WebSocket
public bool IsMuted => VoiceState?.IsMuted ?? false;
/// <inheritdoc />
public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks);
/// <summary>
/// Returns a collection of roles that the user possesses.
/// </summary>
public IReadOnlyCollection<SocketRole> Roles
=> _roleIds.Select(id => Guild.GetRole(id)).Where(x => x != null).ToReadOnlyCollection(() => _roleIds.Length);
/// <summary>
/// Returns the voice channel the user is in, or <see langword="null" /> if none.
/// </summary>
public SocketVoiceChannel VoiceChannel => VoiceState?.VoiceChannel;
/// <inheritdoc />
public string VoiceSessionId => VoiceState?.VoiceSessionId ?? "";
public SocketVoiceState? VoiceState => Guild.GetVoiceState(Id);
public AudioInStream AudioStream => Guild.GetAudioStream(Id);

/// <summary> The position of the user within the role hierarchy. </summary>
/// <remarks> The returned value equal to the position of the highest role the user has,
/// or <see cref="int.MaxValue"/> if user is the server owner. </remarks>
/// <summary>
/// Returns the position of the user within the role hierarchy.
/// </summary>
/// <remarks>
/// The returned value equal to the position of the highest role the user has, or
/// <see cref="int.MaxValue"/> if user is the server owner.
/// </remarks>
public int Hierarchy
{
get


Loading…
Cancel
Save