Browse Source

Merge branch 'dev' into Interactions

pull/1923/head
quin lynch 4 years ago
parent
commit
4d1036950f
100 changed files with 1019 additions and 624 deletions
  1. +122
    -0
      CHANGELOG.md
  2. +1
    -1
      Discord.Net.targets
  3. +1
    -1
      docs/_overwrites/Common/EmbedBuilder.Overwrites.md
  4. +1
    -1
      docs/guides/commands/intro.md
  5. +2
    -6
      docs/guides/getting_started/first-bot.md
  6. +1
    -2
      docs/guides/getting_started/samples/first-bot/async-context.cs
  7. +1
    -2
      docs/guides/getting_started/samples/first-bot/complete.cs
  8. +2
    -2
      docs/guides/getting_started/samples/first-bot/structure.cs
  9. +1
    -1
      samples/03_sharded_client/Modules/PublicModule.cs
  10. +1
    -1
      src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs
  11. +2
    -2
      src/Discord.Net.Commands/CommandService.cs
  12. +2
    -14
      src/Discord.Net.Core/DiscordConfig.cs
  13. +15
    -0
      src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs
  14. +0
    -21
      src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs
  15. +9
    -51
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  16. +2
    -2
      src/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs
  17. +21
    -0
      src/Discord.Net.Core/Entities/Invites/IInvite.cs
  18. +0
    -14
      src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs
  19. +6
    -50
      src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs
  20. +20
    -1
      src/Discord.Net.Core/Entities/Messages/IMessage.cs
  21. +67
    -0
      src/Discord.Net.Core/Entities/Messages/ISticker.cs
  22. +0
    -12
      src/Discord.Net.Core/Entities/Messages/IUserMessage.cs
  23. +36
    -0
      src/Discord.Net.Core/Entities/Messages/MessageFlags.cs
  24. +13
    -0
      src/Discord.Net.Core/Entities/Messages/MessageProperties.cs
  25. +0
    -3
      src/Discord.Net.Core/Entities/Messages/MessageType.cs
  26. +15
    -0
      src/Discord.Net.Core/Entities/Messages/SticketFormatType.cs
  27. +0
    -5
      src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs
  28. +0
    -3
      src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs
  29. +0
    -2
      src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs
  30. +3
    -3
      src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs
  31. +7
    -3
      src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs
  32. +7
    -0
      src/Discord.Net.Core/Entities/Roles/IRole.cs
  33. +40
    -0
      src/Discord.Net.Core/Entities/Roles/RoleTags.cs
  34. +41
    -1
      src/Discord.Net.Core/Entities/Users/IGuildUser.cs
  35. +0
    -4
      src/Discord.Net.Core/Entities/Users/IPresence.cs
  36. +13
    -3
      src/Discord.Net.Core/Entities/Users/IUser.cs
  37. +38
    -10
      src/Discord.Net.Core/Entities/Users/UserProperties.cs
  38. +0
    -10
      src/Discord.Net.Core/Extensions/StringExtensions.cs
  39. +3
    -3
      src/Discord.Net.Core/Extensions/UserExtensions.cs
  40. +11
    -0
      src/Discord.Net.Core/GatewayIntents.cs
  41. +0
    -18
      src/Discord.Net.Core/RateLimitPrecision.cs
  42. +0
    -2
      src/Discord.Net.Core/TokenType.cs
  43. +1
    -1
      src/Discord.Net.Core/Utils/SnowflakeUtils.cs
  44. +2
    -2
      src/Discord.Net.Examples/Core/Entities/Users/IUser.Examples.cs
  45. +5
    -4
      src/Discord.Net.Examples/WebSocket/BaseSocketClient.Events.Examples.cs
  46. +1
    -1
      src/Discord.Net.Rest/API/Common/AllowedMentions.cs
  47. +1
    -1
      src/Discord.Net.Rest/API/Common/AuditLogEntry.cs
  48. +0
    -4
      src/Discord.Net.Rest/API/Common/Guild.cs
  49. +0
    -13
      src/Discord.Net.Rest/API/Common/GuildEmbed.cs
  50. +2
    -0
      src/Discord.Net.Rest/API/Common/GuildMember.cs
  51. +6
    -0
      src/Discord.Net.Rest/API/Common/Invite.cs
  52. +4
    -8
      src/Discord.Net.Rest/API/Common/InviteMetadata.cs
  53. +2
    -0
      src/Discord.Net.Rest/API/Common/InviteVanity.cs
  54. +2
    -0
      src/Discord.Net.Rest/API/Common/Message.cs
  55. +0
    -10
      src/Discord.Net.Rest/API/Common/MessageFlags.cs
  56. +2
    -2
      src/Discord.Net.Rest/API/Common/Overwrite.cs
  57. +0
    -2
      src/Discord.Net.Rest/API/Common/Presence.cs
  58. +4
    -2
      src/Discord.Net.Rest/API/Common/Role.cs
  59. +15
    -0
      src/Discord.Net.Rest/API/Common/RoleTags.cs
  60. +25
    -0
      src/Discord.Net.Rest/API/Common/Sticker.cs
  61. +2
    -0
      src/Discord.Net.Rest/API/Common/User.cs
  62. +2
    -2
      src/Discord.Net.Rest/API/Common/UserGuild.cs
  63. +5
    -5
      src/Discord.Net.Rest/API/Rest/ModifyChannelPermissionsParams.cs
  64. +2
    -2
      src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs
  65. +4
    -0
      src/Discord.Net.Rest/API/Rest/ModifyMessageParams.cs
  66. +16
    -0
      src/Discord.Net.Rest/API/Rest/ModifyWebhookMessageParams.cs
  67. +0
    -11
      src/Discord.Net.Rest/API/Rest/SuppressEmbedParams.cs
  68. +2
    -0
      src/Discord.Net.Rest/API/Rest/UploadFileParams.cs
  69. +12
    -14
      src/Discord.Net.Rest/ClientHelper.cs
  70. +48
    -42
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  71. +14
    -8
      src/Discord.Net.Rest/DiscordRestClient.cs
  72. +9
    -1
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs
  73. +5
    -2
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs
  74. +10
    -4
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs
  75. +10
    -4
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs
  76. +1
    -1
      src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs
  77. +17
    -10
      src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs
  78. +4
    -0
      src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs
  79. +4
    -0
      src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs
  80. +4
    -0
      src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs
  81. +13
    -28
      src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
  82. +3
    -51
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  83. +0
    -25
      src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs
  84. +9
    -0
      src/Discord.Net.Rest/Entities/Invites/RestInvite.cs
  85. +5
    -16
      src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs
  86. +92
    -9
      src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs
  87. +12
    -0
      src/Discord.Net.Rest/Entities/Messages/RestMessage.cs
  88. +19
    -9
      src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs
  89. +48
    -0
      src/Discord.Net.Rest/Entities/Messages/Sticker.cs
  90. +4
    -0
      src/Discord.Net.Rest/Entities/Roles/RestRole.cs
  91. +2
    -2
      src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs
  92. +20
    -4
      src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs
  93. +8
    -4
      src/Discord.Net.Rest/Entities/Users/RestUser.cs
  94. +20
    -10
      src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs
  95. +6
    -6
      src/Discord.Net.Rest/Entities/Users/UserHelper.cs
  96. +4
    -2
      src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs
  97. +7
    -0
      src/Discord.Net.Rest/Extensions/EntityExtensions.cs
  98. +0
    -2
      src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs
  99. +0
    -44
      src/Discord.Net.Rest/Net/Converters/PermissionTargetConverter.cs
  100. +0
    -2
      src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs

+ 122
- 0
CHANGELOG.md View File

@@ -1,5 +1,127 @@
# Changelog

## [2.4.0] - 2021-05-22
### Added
- #1726 Add stickers (91a9063)
- #1753 Webhook message edit & delete functionality (f67cd8e)
- #1757 Add ability to add/remove roles by id (4c9910c)
- #1781 Add GetEmotesAsync to IGuild (df23d57)
- #1801 Add missing property to MESSAGE_REACTION_ADD event (0715d7d)
- #1828 Add methods to interact with reactions without a message object (5b244f2)
- #1830 Add ModifyMessageAsync to IMessageChannel (365a848)
- #1844 Add Discord Certified Moderator user flag (4b8d444)

### Fixed
- #1486 Add type reader when entity type reader exists (c46daaa)
- #1835 Cached message emoji cleanup at MESSAGE_REACTION_REMOVE_EMOJI (8afef82)

### Misc
- #1778 Remove URI check from EmbedBuilder (25b04c4)
- #1800 Fix spelling in SnowflakeUtils.FromSnowflake (6aff419)

## [2.3.1] - 2021-03-10
### Fixed
- #1761 Deadlock in DiscordShardedClient when Ready is never received (73e5cc2)
- #1773 Private methods aren't added as commands (0fc713a)
- #1780 NullReferenceException in pin/unpin audit logs (f794163)
- #1786 Add ChannelType property to ChannelInfo audit log (6ac5ea1)
- #1791 Update Webhook ChannelId from model change (d2518db)
- #1794 Audit log UserId can be null (d41aeee)

### Misc
- #1774 Add remark regarding CustomStatus as the activity (51b7afe)

## [2.3.0] - 2021-01-28
### Added
- #1491 Add INVITE_CREATE and INVITE_DELETE events (1ab670b)
- #1520 Support reading multiple activities (421a0c1)
- #1521 Allow for inherited commands in modules (a51cdf6)
- #1526 Add Direction.Around to GetMessagesAsync (f2130f8)
- #1537 Implement gateway ratelimit (ec673e1)
- #1544 Add MESSAGE_REACTION_REMOVE_EMOJI and RemoveAllReactionsForEmoteAsync (a89f076)
- #1549 Add GetUsersAsync to SocketGuild (30b5a83)
- #1566 Support Gateway Intents (d5d10d3)
- #1573 Add missing properties to Guild and deprecate GuildEmbed (ec212b1)
- #1581 Add includeRoleIds to PruneUsersAsync (a80e5ff)
- #1588 Add GetStreams to AudioClient (1e012ac)
- #1596 Add missing channel properties (2d80037)
- #1604 Add missing application properties (including Teams) (10fcde0)
- #1619 Add "View Guild Insights" to GuildPermission (2592264)
- #1637 Added CultureInvariant RegexOption to WebhookUrlRegex (e3925a7)
- #1659 Add inline replies (e3850e1)
- #1688 Send presence on Identify payload (25d5d36)
- #1721 Add role tags (6a62c47)
- #1722 Add user public flags (c683b29)
- #1724 Add MessageFlags and AllowedMentions to message modify (225550d)
- #1731 Add GuildUser IsPending property (8b25c9b)
- #1690 Add max bitrate value to SocketGuild (aacfea0)

### Fixed
- #1244 Missing AddReactions permission for DM channels. (e40ca4a)
- #1469 unsupported property causes an exception (468f826)
- #1525 AllowedMentions and AllowedMentionTypes (3325031)
- #1531 Add AllowedMentions to SendFileAsync (ab32607)
- #1532 GuildEmbed.ChannelId as nullable per API documentation (971d519)
- #1546 Different ratelimits for the same route (implement discord buckets) (2f6c017)
- #1548 Incomplete Ready, DownloadUsersAsync, and optimize AlwaysDownloadUsers (dc8c959)
- #1555 InvalidOperationException at MESSAGE_CREATE (bd4672a)
- #1557 Sending 2 requests instead of 1 to create a Guild role. (5430cc8)
- #1571 Not using the new domain name. (df8a0f7)
- #1578 Trim token before passing it to the authorization header (42ba372)
- #1580 Stop TaskCanceledException from bubbling up (b8fa464)
- #1599 Invite audit log without inviter (b95b95b)
- #1602 Add AllowedMentions to webhooks (bd4516b)
- #1603 Cancel reconnection when 4014 (f396cd9)
- #1608 Voice overwrites and CategoryId remarks (43c8fc0)
- #1614 Check error 404 and return null for GetBanAsync (ae9fff6)
- #1621 Parse mentions from message payload (366ca9a)
- #1622 Do not update overwrite cache locally (3860da0)
- #1623 Invoke UserUpdated from GuildMemberUpdated if needed (3085e88)
- #1624 Handle null PreferredLocale in rare cases (c1d04b4)
- #1639 Invite and InviteMetadata properties (dd2e524)
- #1642 Add missing permissions (4b389f3)
- #1647 handicap member downloading for verified bots (fa5ef5e)
- #1652 Update README.MD to reflect new discord domain (03b831e)
- #1667 Audio stream dispose (a2af985)
- #1671 Crosspost throwing InvalidOperationException (9134443)
- #1672 Team is nullable, not optional (be60d81)
- #1681 Emoji url encode (04389a4)
- #1683 SocketGuild.HasAllMembers is false if a user left a guild (47f571e)
- #1686 Revert PremiumSubscriptionCount type (97e71cd)
- #1695 Possible NullReferenceException when receiving InvalidSession (5213916)
- #1702 Rollback Activities to Game (9d7cb39)
- #1727 Move and fix internal AllowedMentions object (4a7f8fe)
- limit request members batch size (084db25)
- UserMentions throwing NullRef (5ed01a3)
- Wrong author for SocketUserMessage.ReferencedMessage (1e9b252)
- Discord sends null when there's no team (05a1f0a)
- IMessage.Embeds docs remarks (a4d32d3)
- Missing MessageReference when sending files (2095701)

### Misc
- #1545 MutualGuilds optimization (323a677)
- #1551 Update webhook regex to support discord.com (7585789)
- #1556 Add SearchUsersAsync (57880de)
- #1561 Minor refactor to switch expression (42826df)
- #1576 Updating comments for privileged intents (c42bfa6)
- #1678 Change ratelimit messages (47ed806)
- #1714 Update summary of SocketVoiceChannel.Users (e385c40)
- #1720 VoiceRegions and related changes (5934c79)
- Add updated libraries for LastModified (d761846)
- Add alternative documentation link (accd351)
- Temporarily disable StyleCops until all the fixes are impl'd (36de7b2)
- Remove redundant CreateGuildRoleParams (3df0539)
- Add minor tweaks to DiscordSocketConfig docs strings (2cd1880)
- Fix MaxWaitBetweenGuildAvailablesBeforeReady docs string (e31cdc7)
- Missing summary tag for GatewayIntents (3a10018)
- Add new method of role ID copy (857ef77)
- Resolve inheritdocs for IAttachment (9ea3291)
- Mark null as a specific langword in summary (13a41f8)
- Cleanup GatewayReconnectException docs (833ee42)
- Update Docfx.Plugins.LastModified to v1.2.4 (28a6f97)
- Update framework version for tests to Core 3.1 to comply with LTS (4988a07)
- Move bulk deletes remarks from <summary> to <remarks> (62539f0)

## [2.2.0] - 2020-04-16
### Added
- #1247 Implement Client Status Support (9da11b4)


+ 1
- 1
Discord.Net.targets View File

@@ -1,6 +1,6 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VersionPrefix>2.3.0</VersionPrefix>
<VersionPrefix>3.0.0</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
<LangVersion>latest</LangVersion>
<Authors>Discord.Net Contributors</Authors>


+ 1
- 1
docs/_overwrites/Common/EmbedBuilder.Overwrites.md View File

@@ -28,7 +28,7 @@ public async Task SendRichEmbedAsync()
var embed = new EmbedBuilder
{
// Embed property can be set within object initializer
Title = "Hello world!"
Title = "Hello world!",
Description = "I am a description set by initializer."
};
// Or with methods


+ 1
- 1
docs/guides/commands/intro.md View File

@@ -134,7 +134,7 @@ If, for whatever reason, you have two commands which are ambiguous to
each other, you may use the @Discord.Commands.PriorityAttribute to
specify which should be tested before the other.

The `Priority` attributes are sorted in ascending order; the higher
The `Priority` attributes are sorted in descending order; the higher
priority will be called first.

### Command Context


+ 2
- 6
docs/guides/getting_started/first-bot.md View File

@@ -80,15 +80,11 @@ recommended for these operations to be awaited in a
properly established async context whenever possible.

To establish an async context, we will be creating an async main method
in your console application, and rewriting the static main method to
invoke the new async main.
in your console application.

[!code-csharp[Async Context](samples/first-bot/async-context.cs)]

As a result of this, your program will now start and immediately
jump into an async context. This allows us to create a connection
to Discord later on without having to worry about setting up the
correct async implementation.
As a result of this, your program will now start into an async context.

> [!WARNING]
> If your application throws any exceptions within an async context,


+ 1
- 2
docs/guides/getting_started/samples/first-bot/async-context.cs View File

@@ -1,7 +1,6 @@
public class Program
{
public static void Main(string[] args)
=> new Program().MainAsync().GetAwaiter().GetResult();
public static Task Main(string[] args) => new Program().MainAsync();

public async Task MainAsync()
{


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

@@ -2,8 +2,7 @@ public class Program
{
private DiscordSocketClient _client;
public static void Main(string[] args)
=> new Program().MainAsync().GetAwaiter().GetResult();
public static Task Main(string[] args) => new Program().MainAsync();

public async Task MainAsync()
{


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

@@ -10,11 +10,11 @@ using Discord.WebSocket;
class Program
{
// Program entry point
static void Main(string[] args)
static Task Main(string[] args)
{
// Call the Program constructor, followed by the
// MainAsync method and wait until it finishes (which should be never).
new Program().MainAsync().GetAwaiter().GetResult();
return new Program().MainAsync();
}

private readonly DiscordSocketClient _client;


+ 1
- 1
samples/03_sharded_client/Modules/PublicModule.cs View File

@@ -9,7 +9,7 @@ namespace _03_sharded_client.Modules
[Command("info")]
public async Task InfoAsync()
{
var msg = $@"Hi {Context.User}! There are currently {Context.Client.Shards} shards!
var msg = $@"Hi {Context.User}! There are currently {Context.Client.Shards.Count} shards!
This guild is being served by shard number {Context.Client.GetShardFor(Context.Guild).ShardId}";
await ReplyAsync(msg);
}


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

@@ -136,7 +136,7 @@ namespace Discord.Commands
builder.Name = typeInfo.Name;

// Get all methods (including from inherited members), that are valid commands
var validCommands = typeInfo.GetMethods().Where(IsValidCommandDefinition);
var validCommands = typeInfo.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Where(IsValidCommandDefinition);

foreach (var method in validCommands)
{


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

@@ -408,7 +408,7 @@ namespace Discord.Commands
var typeInfo = type.GetTypeInfo();
if (typeInfo.IsEnum)
return true;
return _entityTypeReaders.Any(x => type == x.EntityType || typeInfo.ImplementedInterfaces.Contains(x.TypeReaderType));
return _entityTypeReaders.Any(x => type == x.EntityType || typeInfo.ImplementedInterfaces.Contains(x.EntityType));
}
internal void AddNullableTypeReader(Type valueType, TypeReader valueTypeReader)
{
@@ -511,7 +511,7 @@ namespace Discord.Commands
await _commandExecutedEvent.InvokeAsync(Optional.Create<CommandInfo>(), context, searchResult).ConfigureAwait(false);
return searchResult;
}

var commands = searchResult.Commands;
var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>();


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

@@ -16,7 +16,7 @@ namespace Discord
/// <see href="https://discord.com/developers/docs/reference#api-versioning">Discord API documentation</see>
/// .</para>
/// </returns>
public const int APIVersion = 6;
public const int APIVersion = 9;
/// <summary>
/// Returns the Voice API version Discord.Net uses.
/// </summary>
@@ -43,7 +43,7 @@ namespace Discord
/// <returns>
/// The user agent used in each Discord.Net request.
/// </returns>
public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})";
public static string UserAgent { get; } = $"DiscordBot (https://github.com/discord-net/Discord.Net, v{Version})";
/// <summary>
/// Returns the base Discord API URL.
/// </summary>
@@ -141,18 +141,6 @@ namespace Discord
/// </remarks>
internal bool DisplayInitialLog { get; set; } = true;

/// <summary>
/// Gets or sets the level of precision of the rate limit reset response.
/// </summary>
/// <remarks>
/// If set to <see cref="RateLimitPrecision.Second"/>, this value will be rounded up to the
/// nearest second.
/// </remarks>
/// <returns>
/// The currently set <see cref="RateLimitPrecision"/>.
/// </returns>
public RateLimitPrecision RateLimitPrecision { get; set; } = RateLimitPrecision.Millisecond;

/// <summary>
/// Gets or sets whether or not rate-limits should use the system clock.
/// </summary>


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

@@ -260,6 +260,21 @@ namespace Discord
/// </returns>
Task DeleteMessageAsync(IMessage message, RequestOptions options = null);

/// <summary>
/// Modifies a message.
/// </summary>
/// <remarks>
/// This method modifies this message with the specified properties. To see an example of this
/// method and what properties are available, please refer to <see cref="MessageProperties"/>.
/// </remarks>
/// <param name="messageId">The snowflake identifier of the message that would be changed.</param>
/// <param name="func">A delegate containing the properties to modify the message with.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous modification operation.
/// </returns>
Task<IUserMessage> ModifyMessageAsync(ulong messageId, Action<MessageProperties> func, RequestOptions options = null);

/// <summary>
/// Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds.
/// </summary>


+ 0
- 21
src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs View File

@@ -1,21 +0,0 @@
namespace Discord
{
/// <summary>
/// Provides properties that are used to modify the widget of an <see cref="IGuild" /> with the specified changes.
/// </summary>
public class GuildEmbedProperties
{
/// <summary>
/// Sets whether the widget should be enabled.
/// </summary>
public Optional<bool> Enabled { get; set; }
/// <summary>
/// Sets the channel that the invite should place its users in, if not <c>null</c>.
/// </summary>
public Optional<IChannel> Channel { get; set; }
/// <summary>
/// Sets the channel the invite should place its users in, if not <c>null</c>.
/// </summary>
public Optional<ulong?> ChannelId { get; set; }
}
}

+ 9
- 51
src/Discord.Net.Core/Entities/Guilds/IGuild.cs View File

@@ -28,13 +28,6 @@ namespace Discord
/// </returns>
int AFKTimeout { get; }
/// <summary>
/// Gets a value that indicates whether this guild is embeddable (i.e. can use widget).
/// </summary>
/// <returns>
/// <see langword="true" /> if this guild has a widget enabled; otherwise <see langword="false" />.
/// </returns>
bool IsEmbeddable { get; }
/// <summary>
/// Gets a value that indicates whether this guild has the widget enabled.
/// </summary>
/// <returns>
@@ -132,29 +125,6 @@ namespace Discord
/// </returns>
ulong? AFKChannelId { get; }
/// <summary>
/// Gets the ID of the default channel for this guild.
/// </summary>
/// <remarks>
/// This property retrieves the snowflake identifier of the first viewable text channel for this guild.
/// <note type="warning">
/// This channel does not guarantee the user can send message to it, as it only looks for the first viewable
/// text channel.
/// </note>
/// </remarks>
/// <returns>
/// A <see langword="ulong"/> representing the snowflake identifier of the default text channel; <c>0</c> if
/// none can be found.
/// </returns>
ulong DefaultChannelId { get; }
/// <summary>
/// Gets the ID of the widget embed channel of this guild.
/// </summary>
/// <returns>
/// A <see langword="ulong"/> representing the snowflake identifier of the embedded channel found within the
/// widget settings of this guild; <see langword="null" /> if none is set.
/// </returns>
ulong? EmbedChannelId { get; }
/// <summary>
/// Gets the ID of the channel assigned to the widget of this guild.
/// </summary>
/// <returns>
@@ -364,16 +334,6 @@ namespace Discord
/// </returns>
Task ModifyAsync(Action<GuildProperties> func, RequestOptions options = null);
/// <summary>
/// Modifies this guild's embed channel.
/// </summary>
/// <param name="func">The delegate containing the properties to modify the guild widget with.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous modification operation.
/// </returns>
[Obsolete("This endpoint is deprecated, use ModifyWidgetAsync instead.")]
Task ModifyEmbedAsync(Action<GuildEmbedProperties> func, RequestOptions options = null);
/// <summary>
/// Modifies this guild's widget.
/// </summary>
/// <param name="func">The delegate containing the properties to modify the guild widget with.</param>
@@ -592,17 +552,6 @@ namespace Discord
/// </returns>
Task<ITextChannel> GetDefaultChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets the embed channel (i.e. the channel set in the guild's widget settings) in this guild.
/// </summary>
/// <param name="mode">The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the embed channel set
/// within the server's widget settings; <see langword="null" /> if none is set.
/// </returns>
[Obsolete("This endpoint is deprecated, use GetWidgetChannelAsync instead.")]
Task<IGuildChannel> GetEmbedChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets the widget channel (i.e. the channel set in the guild's widget settings) in this guild.
/// </summary>
/// <param name="mode">The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.</param>
@@ -892,6 +841,15 @@ namespace Discord
/// </returns>
Task<IReadOnlyCollection<IWebhook>> GetWebhooksAsync(RequestOptions options = null);

/// <summary>
/// Gets a collection of emotes from this guild.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a read-only collection
/// of emotes found within the guild.
/// </returns>
Task<IReadOnlyCollection<GuildEmote>> GetEmotesAsync(RequestOptions options = null);
/// <summary>
/// Gets a specific emote from this guild.
/// </summary>


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

@@ -8,10 +8,10 @@ namespace Discord
/// <summary>
/// The target of the permission is a role.
/// </summary>
Role,
Role = 0,
/// <summary>
/// The target of the permission is a user.
/// </summary>
User
User = 1,
}
}

+ 21
- 0
src/Discord.Net.Core/Entities/Invites/IInvite.cs View File

@@ -20,6 +20,13 @@ namespace Discord
/// </returns>
string Url { get; }

/// <summary>
/// Gets the user that created this invite.
/// </summary>
/// <returns>
/// A user that created this invite.
/// </returns>
IUser Inviter { get; }
/// <summary>
/// Gets the channel this invite is linked to.
/// </summary>
@@ -83,5 +90,19 @@ namespace Discord
/// invite points to; <c>null</c> if one cannot be obtained.
/// </returns>
int? MemberCount { get; }
/// <summary>
/// Gets the user this invite is linked to via <see cref="TargetUserType"/>.
/// </summary>
/// <returns>
/// A user that is linked to this invite.
/// </returns>
IUser TargetUser { get; }
/// <summary>
/// Gets the type of the linked <see cref="TargetUser"/> for this invite.
/// </summary>
/// <returns>
/// The type of the linked user that is linked to this invite.
/// </returns>
TargetUserType TargetUserType { get; }
}
}

+ 0
- 14
src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs View File

@@ -7,20 +7,6 @@ namespace Discord
/// </summary>
public interface IInviteMetadata : IInvite
{
/// <summary>
/// Gets the user that created this invite.
/// </summary>
/// <returns>
/// A user that created this invite.
/// </returns>
IUser Inviter { get; }
/// <summary>
/// Gets a value that indicates whether the invite has been revoked.
/// </summary>
/// <returns>
/// <c>true</c> if this invite was revoked; otherwise <c>false</c>.
/// </returns>
bool IsRevoked { get; }
/// <summary>
/// Gets a value that indicates whether the invite is a temporary one.
/// </summary>


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

@@ -12,7 +12,6 @@ namespace Discord
{
private string _title;
private string _description;
private string _url;
private EmbedImage? _image;
private EmbedThumbnail? _thumbnail;
private List<EmbedFieldBuilder> _fields;
@@ -70,26 +69,14 @@ 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;
set
{
if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI.", paramName: nameof(Url));
_url = value;
}
}
public string Url { get; set; }
/// <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;
set
{
if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI.", paramName: nameof(ThumbnailUrl));
_thumbnail = new EmbedThumbnail(value, null, null, null);
}
set => _thumbnail = new EmbedThumbnail(value, null, null, null);
}
/// <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>
@@ -97,11 +84,7 @@ namespace Discord
public string ImageUrl
{
get => _image?.Url;
set
{
if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI.", paramName: nameof(ImageUrl));
_image = new EmbedImage(value, null, null, null);
}
set => _image = new EmbedImage(value, null, null, null);
}

/// <summary> Gets or sets the list of <see cref="EmbedFieldBuilder"/> of an <see cref="Embed"/>. </summary>
@@ -553,8 +536,6 @@ namespace Discord
public class EmbedAuthorBuilder
{
private string _name;
private string _url;
private string _iconUrl;
/// <summary>
/// Gets the maximum author name length allowed by Discord.
/// </summary>
@@ -585,15 +566,7 @@ namespace Discord
/// <returns>
/// The URL of the author field.
/// </returns>
public string Url
{
get => _url;
set
{
if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI.", paramName: nameof(Url));
_url = value;
}
}
public string Url { get; set; }
/// <summary>
/// Gets or sets the icon URL of the author field.
/// </summary>
@@ -601,15 +574,7 @@ namespace Discord
/// <returns>
/// The icon URL of the author field.
/// </returns>
public string IconUrl
{
get => _iconUrl;
set
{
if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI.", paramName: nameof(IconUrl));
_iconUrl = value;
}
}
public string IconUrl { get; set; }

/// <summary>
/// Sets the name of the author field.
@@ -671,7 +636,6 @@ namespace Discord
public class EmbedFooterBuilder
{
private string _text;
private string _iconUrl;

/// <summary>
/// Gets the maximum footer length allowed by Discord.
@@ -703,15 +667,7 @@ namespace Discord
/// <returns>
/// The icon URL of the footer field.
/// </returns>
public string IconUrl
{
get => _iconUrl;
set
{
if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI.", paramName: nameof(IconUrl));
_iconUrl = value;
}
}
public string IconUrl { get; set; }

/// <summary>
/// Sets the name of the footer field.


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

@@ -92,10 +92,10 @@ namespace Discord
/// Gets all embeds included in this message.
/// </summary>
/// <remarks>
/// </remarks>
/// This property gets a read-only collection of embeds associated with this message. Depending on the
/// message, a sent message may contain one or more embeds. This is usually true when multiple link previews
/// are generated; however, only one <see cref="EmbedType.Rich"/> <see cref="Embed"/> can be featured.
/// </remarks>
/// <returns>
/// A read-only collection of embed objects.
/// </returns>
@@ -168,6 +168,25 @@ namespace Discord
/// The <see cref="IMessageComponent"/>'s attached to this message
/// </summary>
IReadOnlyCollection<IMessageComponent> Components { get; }

/// Gets all stickers included in this message.
/// </summary>
/// <returns>
/// A read-only collection of sticker objects.
/// </returns>
IReadOnlyCollection<ISticker> Stickers { get; }
/// <summary>
/// Gets the flags related to this message.
/// </summary>
/// <remarks>
/// This value is determined by bitwise OR-ing <see cref="MessageFlags"/> values together.
/// </remarks>
/// <returns>
/// A message's flags, if any is associated.
/// </returns>
MessageFlags? Flags { get; }
/// <summary>
/// Adds a reaction to this message.
/// </summary>


+ 67
- 0
src/Discord.Net.Core/Entities/Messages/ISticker.cs View File

@@ -0,0 +1,67 @@
using System.Collections.Generic;

namespace Discord
{
/// <summary>
/// Represents a discord sticker.
/// </summary>
public interface ISticker
{
/// <summary>
/// Gets the ID of this sticker.
/// </summary>
/// <returns>
/// A snowflake ID associated with this sticker.
/// </returns>
ulong Id { get; }
/// <summary>
/// Gets the ID of the pack of this sticker.
/// </summary>
/// <returns>
/// A snowflake ID associated with the pack of this sticker.
/// </returns>
ulong PackId { get; }
/// <summary>
/// Gets the name of this sticker.
/// </summary>
/// <returns>
/// A <see langword="string"/> with the name of this sticker.
/// </returns>
string Name { get; }
/// <summary>
/// Gets the description of this sticker.
/// </summary>
/// <returns>
/// A <see langword="string"/> with the description of this sticker.
/// </returns>
string Description { get; }
/// <summary>
/// Gets the list of tags of this sticker.
/// </summary>
/// <returns>
/// A read-only list with the tags of this sticker.
/// </returns>
IReadOnlyCollection<string> Tags { get; }
/// <summary>
/// Gets the asset hash of this sticker.
/// </summary>
/// <returns>
/// A <see langword="string"/> with the asset hash of this sticker.
/// </returns>
string Asset { get; }
/// <summary>
/// Gets the preview asset hash of this sticker.
/// </summary>
/// <returns>
/// A <see langword="string"/> with the preview asset hash of this sticker.
/// </returns>
string PreviewAsset { get; }
/// <summary>
/// Gets the format type of this sticker.
/// </summary>
/// <returns>
/// A <see cref="StickerFormatType"/> with the format type of this sticker.
/// </returns>
StickerFormatType FormatType { get; }
}
}

+ 0
- 12
src/Discord.Net.Core/Entities/Messages/IUserMessage.cs View File

@@ -36,18 +36,6 @@ namespace Discord
/// </returns>
Task ModifyAsync(Action<MessageProperties> func, RequestOptions options = null);
/// <summary>
/// Modifies the suppression of this message.
/// </summary>
/// <remarks>
/// This method modifies whether or not embeds in this message are suppressed (hidden).
/// </remarks>
/// <param name="suppressEmbeds">Whether or not embeds in this message should be suppressed.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous modification operation.
/// </returns>
Task ModifySuppressionAsync(bool suppressEmbeds, RequestOptions options = null);
/// <summary>
/// Adds this message to its channel's pinned messages.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>


+ 36
- 0
src/Discord.Net.Core/Entities/Messages/MessageFlags.cs View File

@@ -0,0 +1,36 @@
using System;

namespace Discord
{
[Flags]
public enum MessageFlags
{
/// <summary>
/// Default value for flags, when none are given to a message.
/// </summary>
None = 0,
/// <summary>
/// Flag given to messages that have been published to subscribed
/// channels (via Channel Following).
/// </summary>
Crossposted = 1 << 0,
/// <summary>
/// Flag given to messages that originated from a message in another
/// channel (via Channel Following).
/// </summary>
IsCrosspost = 1 << 1,
/// <summary>
/// Flag given to messages that do not display any embeds.
/// </summary>
SuppressEmbeds = 1 << 2,
/// <summary>
/// Flag given to messages that the source message for this crosspost
/// has been deleted (via Channel Following).
/// </summary>
SourceMessageDeleted = 1 << 3,
/// <summary>
/// Flag given to messages that came from the urgent message system.
/// </summary>
Urgent = 1 << 4,
}
}

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

@@ -26,5 +26,18 @@ namespace Discord
/// Gets or sets the components for this message.
/// </summary>
public Optional<MessageComponent> Components { get; set; }

/// <summary>
/// Gets or sets the flags of the message.
/// </summary>
/// <remarks>
/// Only <see cref="MessageFlags.SuppressEmbeds"/> can be set/unset and you need to be
/// the author of the message.
/// </remarks>
public Optional<MessageFlags?> Flags { get; set; }
/// <summary>
/// Gets or sets the allowed mentions of the message.
/// </summary>
public Optional<AllowedMentions> AllowedMentions { get; set; }
}
}

+ 0
- 3
src/Discord.Net.Core/Entities/Messages/MessageType.cs View File

@@ -60,9 +60,6 @@ namespace Discord
/// <summary>
/// The message is an inline reply.
/// </summary>
/// <remarks>
/// Only available in API v8.
/// </remarks>
Reply = 19,
/// <summary>
/// The message is an Application Command


+ 15
- 0
src/Discord.Net.Core/Entities/Messages/SticketFormatType.cs View File

@@ -0,0 +1,15 @@
namespace Discord
{
/// <summary> Defines the types of formats for stickers. </summary>
public enum StickerFormatType
{
/// <summary> Default value for a sticker format type. </summary>
None = 0,
/// <summary> The sticker format type is png. </summary>
Png = 1,
/// <summary> The sticker format type is apng. </summary>
Apng = 2,
/// <summary> The sticker format type is lottie. </summary>
Lottie = 3,
}
}

+ 0
- 5
src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs View File

@@ -22,11 +22,6 @@ namespace Discord
/// </summary>
AddReactions = 0x00_00_00_40,
/// <summary>
/// Allows for reading of messages. This flag is obsolete, use <see cref = "ViewChannel" /> instead.
/// </summary>
[Obsolete("Use ViewChannel instead.")]
ReadMessages = ViewChannel,
/// <summary>
/// Allows guild members to view a channel, which includes reading messages in text channels.
/// </summary>
ViewChannel = 0x00_00_04_00,


+ 0
- 3
src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs View File

@@ -45,9 +45,6 @@ namespace Discord

/// <summary> If <c>true</c>, a user may add reactions. </summary>
public bool AddReactions => Permissions.GetValue(RawValue, ChannelPermission.AddReactions);
/// <summary> If <c>true</c>, a user may join channels. </summary>
[Obsolete("Use ViewChannel instead.")]
public bool ReadMessages => ViewChannel;
/// <summary> If <c>true</c>, a user may view channels. </summary>
public bool ViewChannel => Permissions.GetValue(RawValue, ChannelPermission.ViewChannel);



+ 0
- 2
src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs View File

@@ -65,8 +65,6 @@ namespace Discord
/// Allows for viewing of audit logs.
/// </summary>
ViewAuditLog = 0x00_00_00_80,
[Obsolete("Use ViewChannel instead.")]
ReadMessages = ViewChannel,
ViewChannel = 0x00_00_04_00,
SendMessages = 0x00_00_08_00,
/// <summary>


+ 3
- 3
src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs View File

@@ -37,9 +37,6 @@ namespace Discord
/// <summary> If <c>true</c>, a user may view the guild insights. </summary>
public bool ViewGuildInsights => Permissions.GetValue(RawValue, GuildPermission.ViewGuildInsights);

/// <summary> If True, a user may join channels. </summary>
[Obsolete("Use ViewChannel instead.")]
public bool ReadMessages => ViewChannel;
/// <summary> If True, a user may view channels. </summary>
public bool ViewChannel => Permissions.GetValue(RawValue, GuildPermission.ViewChannel);
/// <summary> If True, a user may send messages. </summary>
@@ -90,6 +87,9 @@ namespace Discord
/// <summary> Creates a new <see cref="GuildPermissions"/> with the provided packed value. </summary>
public GuildPermissions(ulong rawValue) { RawValue = rawValue; }

/// <summary> Creates a new <see cref="GuildPermissions"/> with the provided packed value after converting to ulong. </summary>
public GuildPermissions(string rawValue) { RawValue = ulong.Parse(rawValue); }

private GuildPermissions(ulong initialValue,
bool? createInstantInvite = null,
bool? kickMembers = null,


+ 7
- 3
src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs View File

@@ -43,9 +43,6 @@ namespace Discord
/// <summary> If Allowed, a user may add reactions. </summary>
public PermValue AddReactions => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.AddReactions);
/// <summary> If Allowed, a user may join channels. </summary>
[Obsolete("Use ViewChannel instead.")]
public PermValue ReadMessages => ViewChannel;
/// <summary> If Allowed, a user may join channels. </summary>
public PermValue ViewChannel => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ViewChannel);
/// <summary> If Allowed, a user may send messages. </summary>
public PermValue SendMessages => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.SendMessages);
@@ -93,6 +90,13 @@ namespace Discord
DenyValue = denyValue;
}

/// <summary> Creates a new OverwritePermissions with the provided allow and deny packed values after converting to ulong. </summary>
public OverwritePermissions(string allowValue, string denyValue)
{
AllowValue = ulong.Parse(allowValue);
DenyValue = ulong.Parse(denyValue);
}

private OverwritePermissions(ulong allowValue, ulong denyValue,
PermValue? createInstantInvite = null,
PermValue? manageChannel = null,


+ 7
- 0
src/Discord.Net.Core/Entities/Roles/IRole.cs View File

@@ -65,6 +65,13 @@ namespace Discord
/// An <see cref="int"/> representing the position of the role in the role list of the guild.
/// </returns>
int Position { get; }
/// <summary>
/// Gets the tags related to this role.
/// </summary>
/// <returns>
/// A <see cref="RoleTags"/> object containing all tags related to this role.
/// </returns>
RoleTags Tags { get; }

/// <summary>
/// Modifies this role.


+ 40
- 0
src/Discord.Net.Core/Entities/Roles/RoleTags.cs View File

@@ -0,0 +1,40 @@
namespace Discord
{
/// <summary>
/// Provides tags related to a discord role.
/// </summary>
public class RoleTags
{
/// <summary>
/// Gets the identifier of the bot that this role belongs to, if it does.
/// </summary>
/// <returns>
/// A <see langword="ulong"/> if this role belongs to a bot; otherwise
/// <see langword="null"/>.
/// </returns>
public ulong? BotId { get; }
/// <summary>
/// Gets the identifier of the integration that this role belongs to, if it does.
/// </summary>
/// <returns>
/// A <see langword="ulong"/> if this role belongs to an integration; otherwise
/// <see langword="null"/>.
/// </returns>
public ulong? IntegrationId { get; }
/// <summary>
/// Gets if this role is the guild's premium subscriber (booster) role.
/// </summary>
/// <returns>
/// <see langword="true"/> if this role is the guild's premium subscriber role;
/// otherwise <see langword="false"/>.
/// </returns>
public bool IsPremiumSubscriberRole { get; }

internal RoleTags(ulong? botId, ulong? integrationId, bool isPremiumSubscriber)
{
BotId = botId;
IntegrationId = integrationId;
IsPremiumSubscriberRole = isPremiumSubscriber;
}
}
}

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

@@ -68,6 +68,11 @@ namespace Discord
/// </returns>
IReadOnlyCollection<ulong> RoleIds { get; }

/// <summary>
/// Whether the user has passed the guild's Membership Screening requirements.
/// </summary>
bool? IsPending { get; }

/// <summary>
/// Gets the level permissions granted to this user to a given channel.
/// </summary>
@@ -108,7 +113,15 @@ namespace Discord
/// A task that represents the asynchronous modification operation.
/// </returns>
Task ModifyAsync(Action<GuildUserProperties> func, RequestOptions options = null);

/// <summary>
/// Adds the specified role to this user in the guild.
/// </summary>
/// <param name="roleId">The role to be added to the user.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous role addition operation.
/// </returns>
Task AddRoleAsync(ulong roleId, RequestOptions options = null);
/// <summary>
/// Adds the specified role to this user in the guild.
/// </summary>
@@ -119,6 +132,15 @@ namespace Discord
/// </returns>
Task AddRoleAsync(IRole role, RequestOptions options = null);
/// <summary>
/// Adds the specified <paramref name="roleIds"/> to this user in the guild.
/// </summary>
/// <param name="roleIds">The roles to be added to the user.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous role addition operation.
/// </returns>
Task AddRolesAsync(IEnumerable<ulong> roleIds, RequestOptions options = null);
/// <summary>
/// Adds the specified <paramref name="roles"/> to this user in the guild.
/// </summary>
/// <param name="roles">The roles to be added to the user.</param>
@@ -128,6 +150,15 @@ namespace Discord
/// </returns>
Task AddRolesAsync(IEnumerable<IRole> roles, RequestOptions options = null);
/// <summary>
/// Removes the specified <paramref name="roleId"/> from this user in the guild.
/// </summary>
/// <param name="roleId">The role to be removed from the user.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous role removal operation.
/// </returns>
Task RemoveRoleAsync(ulong roleId, RequestOptions options = null);
/// <summary>
/// Removes the specified <paramref name="role"/> from this user in the guild.
/// </summary>
/// <param name="role">The role to be removed from the user.</param>
@@ -137,6 +168,15 @@ namespace Discord
/// </returns>
Task RemoveRoleAsync(IRole role, RequestOptions options = null);
/// <summary>
/// Removes the specified <paramref name="roleIds"/> from this user in the guild.
/// </summary>
/// <param name="roleIds">The roles to be removed from the user.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous role removal operation.
/// </returns>
Task RemoveRolesAsync(IEnumerable<ulong> roleIds, RequestOptions options = null);
/// <summary>
/// Removes the specified <paramref name="roles"/> from this user in the guild.
/// </summary>
/// <param name="roles">The roles to be removed from the user.</param>


+ 0
- 4
src/Discord.Net.Core/Entities/Users/IPresence.cs View File

@@ -7,10 +7,6 @@ namespace Discord
/// </summary>
public interface IPresence
{
/// <summary>
/// Gets the activity this user is currently doing.
/// </summary>
IActivity Activity { get; }
/// <summary>
/// Gets the current status of this user.
/// </summary>


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

@@ -75,9 +75,19 @@ namespace Discord
/// Gets the username for this user.
/// </summary>
string Username { get; }
/// <summary>
/// Gets the public flags that are applied to this user's account.
/// </summary>
/// <remarks>
/// This value is determined by bitwise OR-ing <see cref="UserProperties"/> values together.
/// </remarks>
/// <returns>
/// The value of public flags for this user.
/// </returns>
UserProperties? PublicFlags { get; }

/// <summary>
/// Gets the direct message channel of this user, or create one if it does not already exist.
/// Creates the direct message channel of this user.
/// </summary>
/// <remarks>
/// This method is used to obtain or create a channel used to send a direct message.
@@ -92,7 +102,7 @@ namespace Discord
/// <example>
/// <para>The following example attempts to send a direct message to the target user and logs the incident should
/// it fail.</para>
/// <code region="GetOrCreateDMChannelAsync" language="cs"
/// <code region="CreateDMChannelAsync" language="cs"
/// source="../../../Discord.Net.Examples/Core/Entities/Users/IUser.Examples.cs"/>
/// </example>
/// <param name="options">The options to be used when sending the request.</param>
@@ -100,6 +110,6 @@ namespace Discord
/// A task that represents the asynchronous operation for getting or creating a DM channel. The task result
/// contains the DM channel associated with this user.
/// </returns>
Task<IDMChannel> GetOrCreateDMChannelAsync(RequestOptions options = null);
Task<IDMChannel> CreateDMChannelAsync(RequestOptions options = null);
}
}

+ 38
- 10
src/Discord.Net.Core/Entities/Users/UserProperties.cs View File

@@ -10,32 +10,60 @@ namespace Discord
/// </summary>
None = 0,
/// <summary>
/// Flag given to Discord staff.
/// Flag given to users who are a Discord employee.
/// </summary>
Staff = 0b1,
Staff = 1 << 0,
/// <summary>
/// Flag given to Discord partners.
/// Flag given to users who are owners of a partnered Discord server.
/// </summary>
Partner = 0b10,
Partner = 1 << 1,
/// <summary>
/// Flag given to users who have participated in the bug report program.
/// Flag given to users in HypeSquad events.
/// </summary>
BugHunter = 0b1000,
HypeSquadEvents = 1 << 2,
/// <summary>
/// Flag given to users who have participated in the bug report program and are level 1.
/// </summary>
BugHunterLevel1 = 1 << 3,
/// <summary>
/// Flag given to users who are in the HypeSquad House of Bravery.
/// </summary>
HypeSquadBravery = 0b100_0000,
HypeSquadBravery = 1 << 6,
/// <summary>
/// Flag given to users who are in the HypeSquad House of Brilliance.
/// </summary>
HypeSquadBrilliance = 0b1000_0000,
HypeSquadBrilliance = 1 << 7,
/// <summary>
/// Flag given to users who are in the HypeSquad House of Balance.
/// </summary>
HypeSquadBalance = 0b1_0000_0000,
HypeSquadBalance = 1 << 8,
/// <summary>
/// Flag given to users who subscribed to Nitro before games were added.
/// </summary>
EarlySupporter = 0b10_0000_0000,
EarlySupporter = 1 << 9,
/// <summary>
/// Flag given to users who are part of a team.
/// </summary>
TeamUser = 1 << 10,
/// <summary>
/// Flag given to users who represent Discord (System).
/// </summary>
System = 1 << 12,
/// <summary>
/// Flag given to users who have participated in the bug report program and are level 2.
/// </summary>
BugHunterLevel2 = 1 << 14,
/// <summary>
/// Flag given to users who are verified bots.
/// </summary>
VerifiedBot = 1 << 16,
/// <summary>
/// Flag given to users that developed bots and early verified their accounts.
/// </summary>
EarlyVerifiedBotDeveloper = 1 << 17,
/// <summary>
/// Flag given to users that are discord certified moderators who has give discord's exam.
/// </summary>
DiscordCertifiedModerator = 1 << 18,
}
}

+ 0
- 10
src/Discord.Net.Core/Extensions/StringExtensions.cs View File

@@ -1,10 +0,0 @@
using System;

namespace Discord
{
internal static class StringExtensions
{
public static bool IsNullOrUri(this string url) =>
string.IsNullOrEmpty(url) || Uri.IsWellFormedUriString(url, UriKind.Absolute);
}
}

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

@@ -43,7 +43,7 @@ namespace Discord
AllowedMentions allowedMentions = null,
MessageComponent component = null)
{
return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options, allowedMentions, component: component).ConfigureAwait(false);
return await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options, allowedMentions, component: component).ConfigureAwait(false);
}

/// <summary>
@@ -95,7 +95,7 @@ namespace Discord
RequestOptions options = null,
MessageComponent component = null)
{
return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(stream, filename, text, isTTS, embed, options, component: component).ConfigureAwait(false);
return await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(stream, filename, text, isTTS, embed, options, component: component).ConfigureAwait(false);
}

/// <summary>
@@ -151,7 +151,7 @@ namespace Discord
RequestOptions options = null,
MessageComponent component = null)
{
return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(filePath, text, isTTS, embed, options, component: component).ConfigureAwait(false);
return await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(filePath, text, isTTS, embed, options, component: component).ConfigureAwait(false);
}

/// <summary>


+ 11
- 0
src/Discord.Net.Core/GatewayIntents.cs View File

@@ -39,5 +39,16 @@ namespace Discord
DirectMessageReactions = 1 << 13,
/// <summary> This intent includes TYPING_START </summary>
DirectMessageTyping = 1 << 14,
/// <summary>
/// This intent includes all but <see cref="GuildMembers"/> and <see cref="GuildMembers"/>
/// that are privileged must be enabled for the application.
/// </summary>
AllUnprivileged = Guilds | GuildBans | GuildEmojis | GuildIntegrations | GuildWebhooks | GuildInvites |
GuildVoiceStates | GuildMessages | GuildMessageReactions | GuildMessageTyping | DirectMessages |
DirectMessageReactions | DirectMessageTyping,
/// <summary>
/// This intent includes all of them, including privileged ones.
/// </summary>
All = AllUnprivileged | GuildMembers | GuildPresences
}
}

+ 0
- 18
src/Discord.Net.Core/RateLimitPrecision.cs View File

@@ -1,18 +0,0 @@
namespace Discord
{
/// <summary>
/// Specifies the level of precision to request in the rate limit
/// response header.
/// </summary>
public enum RateLimitPrecision
{
/// <summary>
/// Specifies precision rounded up to the nearest whole second
/// </summary>
Second,
/// <summary>
/// Specifies precision rounded to the nearest millisecond.
/// </summary>
Millisecond
}
}

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

@@ -5,8 +5,6 @@ namespace Discord
/// <summary> Specifies the type of token to use with the client. </summary>
public enum TokenType
{
[Obsolete("User logins are deprecated and may result in a ToS strike against your account - please see https://github.com/RogueException/Discord.Net/issues/827", error: true)]
User,
/// <summary>
/// An OAuth2 token type.
/// </summary>


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

@@ -12,7 +12,7 @@ namespace Discord
/// </summary>
/// <param name="value">The snowflake identifier to resolve.</param>
/// <returns>
/// A <see cref="DateTimeOffset" /> representing the time for when the object is geenrated.
/// A <see cref="DateTimeOffset" /> representing the time for when the object is generated.
/// </returns>
public static DateTimeOffset FromSnowflake(ulong value)
=> DateTimeOffset.FromUnixTimeMilliseconds((long)((value >> 22) + 1420070400000UL));


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

@@ -18,11 +18,11 @@ namespace Discord.Net.Examples.Core.Entities.Users

#endregion

#region GetOrCreateDMChannelAsync
#region CreateDMChannelAsync

public async Task MessageUserAsync(IUser user)
{
var channel = await user.GetOrCreateDMChannelAsync();
var channel = await user.CreateDMChannelAsync();
try
{
await channel.SendMessageAsync("Awesome stuff!");


+ 5
- 4
src/Discord.Net.Examples/WebSocket/BaseSocketClient.Events.Examples.cs View File

@@ -15,7 +15,7 @@ namespace Discord.Net.Examples.WebSocket
=> client.ReactionAdded += HandleReactionAddedAsync;

public async Task HandleReactionAddedAsync(Cacheable<IUserMessage, ulong> cachedMessage,
ISocketMessageChannel originChannel, SocketReaction reaction)
Cacheable<IMessageChannel, ulong> originChannel, SocketReaction reaction)
{
var message = await cachedMessage.GetOrDownloadAsync();
if (message != null && reaction.User.IsSpecified)
@@ -100,16 +100,17 @@ namespace Discord.Net.Examples.WebSocket
public void HookMessageDeleted(BaseSocketClient client)
=> client.MessageDeleted += HandleMessageDelete;

public Task HandleMessageDelete(Cacheable<IMessage, ulong> cachedMessage, ISocketMessageChannel channel)
public async Task HandleMessageDelete(Cacheable<IMessage, ulong> cachedMessage, Cacheable<IMessageChannel, ulong> cachedChannel)
{
// check if the message exists in cache; if not, we cannot report what was removed
if (!cachedMessage.HasValue) return Task.CompletedTask;
if (!cachedMessage.HasValue) return;
// gets or downloads the channel if it's not in the cache
IMessageChannel channel = await cachedChannel.GetOrDownloadAsync();
var message = cachedMessage.Value;
Console.WriteLine(
$"A message ({message.Id}) from {message.Author} was removed from the channel {channel.Name} ({channel.Id}):"
+ Environment.NewLine
+ message.Content);
return Task.CompletedTask;
}

#endregion


src/Discord.Net.Rest/Entities/Messages/AllowedMentions.cs → src/Discord.Net.Rest/API/Common/AllowedMentions.cs View File

@@ -2,7 +2,7 @@ using Newtonsoft.Json;

namespace Discord.API
{
public class AllowedMentions
internal class AllowedMentions
{
[JsonProperty("parse")]
public Optional<string[]> Parse { get; set; }

+ 1
- 1
src/Discord.Net.Rest/API/Common/AuditLogEntry.cs View File

@@ -7,7 +7,7 @@ namespace Discord.API
[JsonProperty("target_id")]
public ulong? TargetId { get; set; }
[JsonProperty("user_id")]
public ulong UserId { get; set; }
public ulong? UserId { get; set; }

[JsonProperty("changes")]
public AuditLogChange[] Changes { get; set; }


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

@@ -23,10 +23,6 @@ namespace Discord.API
public ulong? AFKChannelId { get; set; }
[JsonProperty("afk_timeout")]
public int AFKTimeout { get; set; }
[JsonProperty("embed_enabled")]
public Optional<bool> EmbedEnabled { get; set; }
[JsonProperty("embed_channel_id")]
public Optional<ulong?> EmbedChannelId { get; set; }
[JsonProperty("verification_level")]
public VerificationLevel VerificationLevel { get; set; }
[JsonProperty("default_message_notifications")]


+ 0
- 13
src/Discord.Net.Rest/API/Common/GuildEmbed.cs View File

@@ -1,13 +0,0 @@
#pragma warning disable CS1591
using Newtonsoft.Json;

namespace Discord.API
{
internal class GuildEmbed
{
[JsonProperty("enabled")]
public bool Enabled { get; set; }
[JsonProperty("channel_id")]
public ulong? ChannelId { get; set; }
}
}

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

@@ -18,6 +18,8 @@ namespace Discord.API
public Optional<bool> Deaf { get; set; }
[JsonProperty("mute")]
public Optional<bool> Mute { get; set; }
[JsonProperty("pending")]
public Optional<bool> Pending { get; set; }
[JsonProperty("premium_since")]
public Optional<DateTimeOffset?> PremiumSince { get; set; }
}


+ 6
- 0
src/Discord.Net.Rest/API/Common/Invite.cs View File

@@ -11,9 +11,15 @@ namespace Discord.API
public Optional<InviteGuild> Guild { get; set; }
[JsonProperty("channel")]
public InviteChannel Channel { get; set; }
[JsonProperty("inviter")]
public Optional<User> Inviter { get; set; }
[JsonProperty("approximate_presence_count")]
public Optional<int?> PresenceCount { get; set; }
[JsonProperty("approximate_member_count")]
public Optional<int?> MemberCount { get; set; }
[JsonProperty("target_user")]
public Optional<User> TargetUser { get; set; }
[JsonProperty("target_user_type")]
public Optional<TargetUserType> TargetUserType { get; set; }
}
}

+ 4
- 8
src/Discord.Net.Rest/API/Common/InviteMetadata.cs View File

@@ -6,19 +6,15 @@ namespace Discord.API
{
internal class InviteMetadata : Invite
{
[JsonProperty("inviter")]
public User Inviter { get; set; }
[JsonProperty("uses")]
public Optional<int> Uses { get; set; }
public int Uses { get; set; }
[JsonProperty("max_uses")]
public Optional<int> MaxUses { get; set; }
public int MaxUses { get; set; }
[JsonProperty("max_age")]
public Optional<int> MaxAge { get; set; }
public int MaxAge { get; set; }
[JsonProperty("temporary")]
public bool Temporary { get; set; }
[JsonProperty("created_at")]
public Optional<DateTimeOffset> CreatedAt { get; set; }
[JsonProperty("revoked")]
public bool Revoked { get; set; }
public DateTimeOffset CreatedAt { get; set; }
}
}

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

@@ -6,5 +6,7 @@ namespace Discord.API
{
[JsonProperty("code")]
public string Code { get; set; }
[JsonProperty("uses")]
public int Uses { get; set; }
}
}

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

@@ -60,5 +60,7 @@ namespace Discord.API
public Optional<Message> ReferencedMessage { get; set; }
[JsonProperty("components")]
public Optional<API.ActionRowComponent[]> Components { get; set; }
[JsonProperty("stickers")]
public Optional<Sticker[]> Stickers { get; set; }
}
}

+ 0
- 10
src/Discord.Net.Rest/API/Common/MessageFlags.cs View File

@@ -1,10 +0,0 @@
using System;

namespace Discord.API
{
[Flags]
internal enum MessageFlags : byte // probably safe to constrain this to 8 values, if not, it's internal so who cares
{
Suppressed = 0x04,
}
}

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

@@ -10,8 +10,8 @@ namespace Discord.API
[JsonProperty("type")]
public PermissionTarget TargetType { get; set; }
[JsonProperty("deny"), Int53]
public ulong Deny { get; set; }
public string Deny { get; set; }
[JsonProperty("allow"), Int53]
public ulong Allow { get; set; }
public string Allow { get; set; }
}
}

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

@@ -13,8 +13,6 @@ namespace Discord.API
public Optional<ulong> GuildId { get; set; }
[JsonProperty("status")]
public UserStatus Status { get; set; }
[JsonProperty("game")]
public Game Game { get; set; }

[JsonProperty("roles")]
public Optional<ulong[]> Roles { get; set; }


+ 4
- 2
src/Discord.Net.Rest/API/Common/Role.cs View File

@@ -1,4 +1,4 @@
#pragma warning disable CS1591
#pragma warning disable CS1591
using Newtonsoft.Json;

namespace Discord.API
@@ -18,8 +18,10 @@ namespace Discord.API
[JsonProperty("position")]
public int Position { get; set; }
[JsonProperty("permissions"), Int53]
public ulong Permissions { get; set; }
public string Permissions { get; set; }
[JsonProperty("managed")]
public bool Managed { get; set; }
[JsonProperty("tags")]
public Optional<RoleTags> Tags { get; set; }
}
}

+ 15
- 0
src/Discord.Net.Rest/API/Common/RoleTags.cs View File

@@ -0,0 +1,15 @@
#pragma warning disable CS1591
using Newtonsoft.Json;

namespace Discord.API
{
internal class RoleTags
{
[JsonProperty("bot_id")]
public Optional<ulong> BotId { get; set; }
[JsonProperty("integration_id")]
public Optional<ulong> IntegrationId { get; set; }
[JsonProperty("premium_subscriber")]
public Optional<bool?> IsPremiumSubscriber { get; set; }
}
}

+ 25
- 0
src/Discord.Net.Rest/API/Common/Sticker.cs View File

@@ -0,0 +1,25 @@
#pragma warning disable CS1591
using Newtonsoft.Json;

namespace Discord.API
{
internal class Sticker
{
[JsonProperty("id")]
public ulong Id { get; set; }
[JsonProperty("pack_id")]
public ulong PackId { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("description")]
public string Desription { get; set; }
[JsonProperty("tags")]
public Optional<string> Tags { get; set; }
[JsonProperty("asset")]
public string Asset { get; set; }
[JsonProperty("preview_asset")]
public string PreviewAsset { get; set; }
[JsonProperty("format_type")]
public StickerFormatType FormatType { get; set; }
}
}

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

@@ -29,5 +29,7 @@ namespace Discord.API
public Optional<PremiumType> PremiumType { get; set; }
[JsonProperty("locale")]
public Optional<string> Locale { get; set; }
[JsonProperty("public_flags")]
public Optional<UserProperties> PublicFlags { get; set; }
}
}

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

@@ -1,4 +1,4 @@
#pragma warning disable CS1591
#pragma warning disable CS1591
using Newtonsoft.Json;

namespace Discord.API
@@ -14,6 +14,6 @@ namespace Discord.API
[JsonProperty("owner")]
public bool Owner { get; set; }
[JsonProperty("permissions"), Int53]
public ulong Permissions { get; set; }
public string Permissions { get; set; }
}
}

+ 5
- 5
src/Discord.Net.Rest/API/Rest/ModifyChannelPermissionsParams.cs View File

@@ -1,4 +1,4 @@
#pragma warning disable CS1591
#pragma warning disable CS1591
using Newtonsoft.Json;

namespace Discord.API.Rest
@@ -7,13 +7,13 @@ namespace Discord.API.Rest
internal class ModifyChannelPermissionsParams
{
[JsonProperty("type")]
public string Type { get; }
public int Type { get; }
[JsonProperty("allow")]
public ulong Allow { get; }
public string Allow { get; }
[JsonProperty("deny")]
public ulong Deny { get; }
public string Deny { get; }

public ModifyChannelPermissionsParams(string type, ulong allow, ulong deny)
public ModifyChannelPermissionsParams(int type, string allow, string deny)
{
Type = type;
Allow = allow;


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

@@ -1,4 +1,4 @@
#pragma warning disable CS1591
#pragma warning disable CS1591
using Newtonsoft.Json;

namespace Discord.API.Rest
@@ -9,7 +9,7 @@ namespace Discord.API.Rest
[JsonProperty("name")]
public Optional<string> Name { get; set; }
[JsonProperty("permissions")]
public Optional<ulong> Permissions { get; set; }
public Optional<string> Permissions { get; set; }
[JsonProperty("color")]
public Optional<uint> Color { get; set; }
[JsonProperty("hoist")]


+ 4
- 0
src/Discord.Net.Rest/API/Rest/ModifyMessageParams.cs View File

@@ -12,5 +12,9 @@ namespace Discord.API.Rest
public Optional<Embed> Embed { get; set; }
[JsonProperty("components")]
public Optional<API.ActionRowComponent[]> Components { get; set; }
[JsonProperty("flags")]
public Optional<MessageFlags?> Flags { get; set; }
[JsonProperty("allowed_mentions")]
public Optional<AllowedMentions> AllowedMentions { get; set; }
}
}

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

@@ -0,0 +1,16 @@
#pragma warning disable CS1591
using Newtonsoft.Json;

namespace Discord.API.Rest
{
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
internal class ModifyWebhookMessageParams
{
[JsonProperty("content")]
public Optional<string> Content { get; set; }
[JsonProperty("embeds")]
public Optional<Embed[]> Embeds { get; set; }
[JsonProperty("allowed_mentions")]
public Optional<AllowedMentions> AllowedMentions { get; set; }
}
}

+ 0
- 11
src/Discord.Net.Rest/API/Rest/SuppressEmbedParams.cs View File

@@ -1,11 +0,0 @@
using Newtonsoft.Json;

namespace Discord.API.Rest
{
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
internal class SuppressEmbedParams
{
[JsonProperty("suppress")]
public bool Suppressed { get; set; }
}
}

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

@@ -52,6 +52,8 @@ namespace Discord.API.Rest
payload["components"] = MessageComponent.Value;
if (IsSpoiler)
payload["hasSpoiler"] = IsSpoiler.ToString();
if (MessageReference.IsSpecified)
payload["message_reference"] = MessageReference.Value;

var json = new StringBuilder();
using (var text = new StringWriter(json))


+ 12
- 14
src/Discord.Net.Rest/ClientHelper.cs View File

@@ -17,7 +17,7 @@ namespace Discord.Rest
return RestApplication.Create(client, model);
}

public static async Task<RestChannel> GetChannelAsync(BaseDiscordClient client,
public static async Task<RestChannel> GetChannelAsync(BaseDiscordClient client,
ulong id, RequestOptions options)
{
var model = await client.ApiClient.GetChannelAsync(id, options).ConfigureAwait(false);
@@ -45,13 +45,13 @@ namespace Discord.Rest
.Where(x => x.Type == ChannelType.Group)
.Select(x => RestGroupChannel.Create(client, x)).ToImmutableArray();
}
public static async Task<IReadOnlyCollection<RestConnection>> GetConnectionsAsync(BaseDiscordClient client, RequestOptions options)
{
var models = await client.ApiClient.GetMyConnectionsAsync(options).ConfigureAwait(false);
return models.Select(RestConnection.Create).ToImmutableArray();
}
public static async Task<RestInviteMetadata> GetInviteAsync(BaseDiscordClient client,
string inviteId, RequestOptions options)
{
@@ -60,7 +60,7 @@ namespace Discord.Rest
return RestInviteMetadata.Create(client, null, null, model);
return null;
}
public static async Task<RestGuild> GetGuildAsync(BaseDiscordClient client,
ulong id, bool withCounts, RequestOptions options)
{
@@ -69,14 +69,6 @@ namespace Discord.Rest
return RestGuild.Create(client, model);
return null;
}
public static async Task<RestGuildEmbed?> GetGuildEmbedAsync(BaseDiscordClient client,
ulong id, RequestOptions options)
{
var model = await client.ApiClient.GetGuildEmbedAsync(id, options).ConfigureAwait(false);
if (model != null)
return RestGuildEmbed.Create(model);
return null;
}
public static async Task<RestGuildWidget?> GetGuildWidgetAsync(BaseDiscordClient client,
ulong id, RequestOptions options)
{
@@ -85,7 +77,7 @@ namespace Discord.Rest
return RestGuildWidget.Create(model);
return null;
}
public static IAsyncEnumerable<IReadOnlyCollection<RestUserGuild>> GetGuildSummariesAsync(BaseDiscordClient client,
public static IAsyncEnumerable<IReadOnlyCollection<RestUserGuild>> GetGuildSummariesAsync(BaseDiscordClient client,
ulong? fromGuildId, int? limit, RequestOptions options)
{
return new PagedAsyncEnumerable<RestUserGuild>(
@@ -136,7 +128,7 @@ namespace Discord.Rest
var model = await client.ApiClient.CreateGuildAsync(args, options).ConfigureAwait(false);
return RestGuild.Create(client, model);
}
public static async Task<RestUser> GetUserAsync(BaseDiscordClient client,
ulong id, RequestOptions options)
{
@@ -221,5 +213,11 @@ namespace Discord.Rest

return response.Select(x => RestGuildCommand.Create(client, x, guildId)).ToImmutableArray();
}

public static Task AddRoleAsync(BaseDiscordClient client, ulong guildId, ulong userId, ulong roleId, RequestOptions options = null)
=> client.ApiClient.AddRoleAsync(guildId, userId, roleId, options);
public static Task RemoveRoleAsync(BaseDiscordClient client, ulong guildId, ulong userId, ulong roleId, RequestOptions options = null)
=> client.ApiClient.RemoveRoleAsync(guildId, userId, roleId, options);
}
}

+ 48
- 42
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -45,19 +45,17 @@ namespace Discord.API
internal string AuthToken { get; private set; }
internal IRestClient RestClient { get; private set; }
internal ulong? CurrentUserId { get; set; }
public RateLimitPrecision RateLimitPrecision { get; private set; }
internal bool UseSystemClock { get; set; }
internal JsonSerializer Serializer => _serializer;

/// <exception cref="ArgumentException">Unknown OAuth token type.</exception>
public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, RetryMode defaultRetryMode = RetryMode.AlwaysRetry,
JsonSerializer serializer = null, RateLimitPrecision rateLimitPrecision = RateLimitPrecision.Second, bool useSystemClock = true)
JsonSerializer serializer = null, bool useSystemClock = true)
{
_restClientProvider = restClientProvider;
UserAgent = userAgent;
DefaultRetryMode = defaultRetryMode;
_serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() };
RateLimitPrecision = rateLimitPrecision;
UseSystemClock = useSystemClock;

RequestQueue = new RequestQueue();
@@ -74,14 +72,12 @@ namespace Discord.API
RestClient.SetHeader("accept", "*/*");
RestClient.SetHeader("user-agent", UserAgent);
RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken));
RestClient.SetHeader("X-RateLimit-Precision", RateLimitPrecision.ToString().ToLower());
}
/// <exception cref="ArgumentException">Unknown OAuth token type.</exception>
internal static string GetPrefixedToken(TokenType tokenType, string token)
{
return tokenType switch
{
default(TokenType) => token,
TokenType.Bot => $"Bot {token}",
TokenType.Bearer => $"Bearer {token}",
_ => throw new ArgumentException(message: "Unknown OAuth token type.", paramName: nameof(tokenType)),
@@ -522,6 +518,43 @@ namespace Discord.API
var ids = new BucketIds(webhookId: webhookId);
return await SendJsonAsync<Message>("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
}

/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
/// <exception cref="InvalidOperationException">This operation may only be called with a <see cref="TokenType.Webhook"/> token.</exception>
public async Task ModifyWebhookMessageAsync(ulong webhookId, ulong messageId, ModifyWebhookMessageParams args, RequestOptions options = null)
{
if (AuthTokenType != TokenType.Webhook)
throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token.");

Preconditions.NotNull(args, nameof(args));
Preconditions.NotEqual(webhookId, 0, nameof(webhookId));
Preconditions.NotEqual(messageId, 0, nameof(messageId));

if (args.Embeds.IsSpecified)
Preconditions.AtMost(args.Embeds.Value.Length, 10, nameof(args.Embeds), "A max of 10 Embeds are allowed.");
if (args.Content.IsSpecified && args.Content.Value.Length > DiscordConfig.MaxMessageSize)
throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content));
options = RequestOptions.CreateOrClone(options);

var ids = new BucketIds(webhookId: webhookId);
await SendJsonAsync<Message>("PATCH", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
}

/// <exception cref="InvalidOperationException">This operation may only be called with a <see cref="TokenType.Webhook"/> token.</exception>
public async Task DeleteWebhookMessageAsync(ulong webhookId, ulong messageId, RequestOptions options = null)
{
if (AuthTokenType != TokenType.Webhook)
throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token.");

Preconditions.NotEqual(webhookId, 0, nameof(webhookId));
Preconditions.NotEqual(messageId, 0, nameof(messageId));

options = RequestOptions.CreateOrClone(options);

var ids = new BucketIds(webhookId: webhookId);
await SendAsync("DELETE", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}", ids, options: options).ConfigureAwait(false);
}

/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public async Task<Message> UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null)
{
@@ -607,16 +640,6 @@ namespace Discord.API
return await SendJsonAsync<Message>("PATCH", () => $"channels/{channelId}/messages/{messageId}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
}

public async Task SuppressEmbedAsync(ulong channelId, ulong messageId, Rest.SuppressEmbedParams args, RequestOptions options = null)
{
Preconditions.NotEqual(channelId, 0, nameof(channelId));
Preconditions.NotEqual(messageId, 0, nameof(messageId));
options = RequestOptions.CreateOrClone(options);

var ids = new BucketIds(channelId: channelId);
await SendJsonAsync("POST", () => $"channels/{channelId}/messages/{messageId}/suppress-embeds", args, ids, options: options).ConfigureAwait(false);
}

public async Task AddReactionAsync(ulong channelId, ulong messageId, string emoji, RequestOptions options = null)
{
Preconditions.NotEqual(channelId, 0, nameof(channelId));
@@ -1170,7 +1193,7 @@ namespace Discord.API

var ids = new BucketIds(guildId: guildId);
string reason = string.IsNullOrWhiteSpace(args.Reason) ? "" : $"&reason={Uri.EscapeDataString(args.Reason)}";
await SendAsync("PUT", () => $"guilds/{guildId}/bans/{userId}?delete-message-days={args.DeleteMessageDays}{reason}", ids, options: options).ConfigureAwait(false);
await SendAsync("PUT", () => $"guilds/{guildId}/bans/{userId}?delete_message_days={args.DeleteMessageDays}{reason}", ids, options: options).ConfigureAwait(false);
}
/// <exception cref="ArgumentException"><paramref name="guildId"/> and <paramref name="userId"/> must not be equal to zero.</exception>
public async Task RemoveGuildBanAsync(ulong guildId, ulong userId, RequestOptions options = null)
@@ -1183,32 +1206,6 @@ namespace Discord.API
await SendAsync("DELETE", () => $"guilds/{guildId}/bans/{userId}", ids, options: options).ConfigureAwait(false);
}

//Guild Embeds
/// <exception cref="ArgumentException"><paramref name="guildId"/> must not be equal to zero.</exception>
public async Task<GuildEmbed> GetGuildEmbedAsync(ulong guildId, RequestOptions options = null)
{
Preconditions.NotEqual(guildId, 0, nameof(guildId));
options = RequestOptions.CreateOrClone(options);

try
{
var ids = new BucketIds(guildId: guildId);
return await SendAsync<GuildEmbed>("GET", () => $"guilds/{guildId}/embed", ids, options: options).ConfigureAwait(false);
}
catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; }
}
/// <exception cref="ArgumentException"><paramref name="guildId"/> must not be equal to zero.</exception>
/// <exception cref="ArgumentNullException"><paramref name="args"/> must not be <see langword="null"/>.</exception>
public async Task<GuildEmbed> ModifyGuildEmbedAsync(ulong guildId, Rest.ModifyGuildEmbedParams args, RequestOptions options = null)
{
Preconditions.NotNull(args, nameof(args));
Preconditions.NotEqual(guildId, 0, nameof(guildId));
options = RequestOptions.CreateOrClone(options);

var ids = new BucketIds(guildId: guildId);
return await SendJsonAsync<GuildEmbed>("PATCH", () => $"guilds/{guildId}/embed", args, ids, options: options).ConfigureAwait(false);
}

//Guild Widget
/// <exception cref="ArgumentException"><paramref name="guildId"/> must not be equal to zero.</exception>
public async Task<GuildWidget> GetGuildWidgetAsync(ulong guildId, RequestOptions options = null)
@@ -1514,6 +1511,15 @@ namespace Discord.API
}

//Guild emoji
public async Task<IReadOnlyCollection<Emoji>> GetGuildEmotesAsync(ulong guildId, RequestOptions options = null)
{
Preconditions.NotEqual(guildId, 0, nameof(guildId));
options = RequestOptions.CreateOrClone(options);

var ids = new BucketIds(guildId: guildId);
return await SendAsync<IReadOnlyCollection<Emoji>>("GET", () => $"guilds/{guildId}/emojis", ids, options: options).ConfigureAwait(false);
}

public async Task<Emoji> GetGuildEmoteAsync(ulong guildId, ulong emoteId, RequestOptions options = null)
{
Preconditions.NotEqual(guildId, 0, nameof(guildId));


+ 14
- 8
src/Discord.Net.Rest/DiscordRestClient.cs View File

@@ -29,10 +29,7 @@ namespace Discord.Rest
internal DiscordRestClient(DiscordRestConfig config, API.DiscordRestApiClient api) : base(config, api) { }

private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config)
=> new API.DiscordRestApiClient(config.RestClientProvider,
DiscordRestConfig.UserAgent,
rateLimitPrecision: config.RateLimitPrecision,
useSystemClock: config.UseSystemClock);
=> new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent, useSystemClock: config.UseSystemClock);

internal override void Dispose(bool disposing)
{
@@ -80,9 +77,6 @@ namespace Discord.Rest
=> ClientHelper.GetGuildAsync(this, id, false, options);
public Task<RestGuild> GetGuildAsync(ulong id, bool withCounts, RequestOptions options = null)
=> ClientHelper.GetGuildAsync(this, id, withCounts, options);
[Obsolete("This endpoint is deprecated, use GetGuildWidgetAsync instead.")]
public Task<RestGuildEmbed?> GetGuildEmbedAsync(ulong id, RequestOptions options = null)
=> ClientHelper.GetGuildEmbedAsync(this, id, options);
public Task<RestGuildWidget?> GetGuildWidgetAsync(ulong id, RequestOptions options = null)
=> ClientHelper.GetGuildWidgetAsync(this, id, options);
public IAsyncEnumerable<IReadOnlyCollection<RestUserGuild>> GetGuildSummariesAsync(RequestOptions options = null)
@@ -119,7 +113,19 @@ namespace Discord.Rest
=> ClientHelper.GetGlobalApplicationCommands(this, options);
public Task<IReadOnlyCollection<RestGuildCommand>> GetGuildApplicationCommands(ulong guildId, RequestOptions options = null)
=> ClientHelper.GetGuildApplicationCommands(this, guildId, options);

public Task AddRoleAsync(ulong guildId, ulong userId, ulong roleId)
=> ClientHelper.AddRoleAsync(this, guildId, userId, roleId);
public Task RemoveRoleAsync(ulong guildId, ulong userId, ulong roleId)
=> ClientHelper.RemoveRoleAsync(this, guildId, userId, roleId);

public Task AddReactionAsync(ulong channelId, ulong messageId, IEmote emote, RequestOptions options = null)
=> MessageHelper.AddReactionAsync(channelId, messageId, emote, this, options);
public Task RemoveReactionAsync(ulong channelId, ulong messageId, ulong userId, IEmote emote, RequestOptions options = null)
=> MessageHelper.RemoveReactionAsync(channelId, messageId, userId, emote, this, options);
public Task RemoveAllReactionsAsync(ulong channelId, ulong messageId, RequestOptions options = null)
=> MessageHelper.RemoveAllReactionsAsync(channelId, messageId, this, options);
public Task RemoveAllReactionsForEmoteAsync(ulong channelId, ulong messageId, IEmote emote, RequestOptions options = null)
=> MessageHelper.RemoveAllReactionsForEmoteAsync(channelId, messageId, emote, this, options);
//IDiscordClient
/// <inheritdoc />
async Task<IApplication> IDiscordClient.GetApplicationInfoAsync(RequestOptions options)


+ 9
- 1
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs View File

@@ -5,13 +5,14 @@ namespace Discord.Rest
/// </summary>
public struct ChannelInfo
{
internal ChannelInfo(string name, string topic, int? rateLimit, bool? nsfw, int? bitrate)
internal ChannelInfo(string name, string topic, int? rateLimit, bool? nsfw, int? bitrate, ChannelType? type)
{
Name = name;
Topic = topic;
SlowModeInterval = rateLimit;
IsNsfw = nsfw;
Bitrate = bitrate;
ChannelType = type;
}

/// <summary>
@@ -53,5 +54,12 @@ namespace Discord.Rest
/// <c>null</c> if this is not mentioned in this entry.
/// </returns>
public int? Bitrate { get; }
/// <summary>
/// Gets the type of this channel.
/// </summary>
/// <returns>
/// The channel type of this channel; <c>null</c> if not applicable.
/// </returns>
public ChannelType? ChannelType { get; }
}
}

+ 5
- 2
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs View File

@@ -26,6 +26,7 @@ namespace Discord.Rest
var rateLimitPerUserModel = changes.FirstOrDefault(x => x.ChangedProperty == "rate_limit_per_user");
var nsfwModel = changes.FirstOrDefault(x => x.ChangedProperty == "nsfw");
var bitrateModel = changes.FirstOrDefault(x => x.ChangedProperty == "bitrate");
var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type");

string oldName = nameModel?.OldValue?.ToObject<string>(discord.ApiClient.Serializer),
newName = nameModel?.NewValue?.ToObject<string>(discord.ApiClient.Serializer);
@@ -37,9 +38,11 @@ namespace Discord.Rest
newNsfw = nsfwModel?.NewValue?.ToObject<bool>(discord.ApiClient.Serializer);
int? oldBitrate = bitrateModel?.OldValue?.ToObject<int>(discord.ApiClient.Serializer),
newBitrate = bitrateModel?.NewValue?.ToObject<int>(discord.ApiClient.Serializer);
ChannelType? oldType = typeModel?.OldValue?.ToObject<ChannelType>(discord.ApiClient.Serializer),
newType = typeModel?.NewValue?.ToObject<ChannelType>(discord.ApiClient.Serializer);

var before = new ChannelInfo(oldName, oldTopic, oldRateLimitPerUser, oldNsfw, oldBitrate);
var after = new ChannelInfo(newName, newTopic, newRateLimitPerUser, newNsfw, newBitrate);
var before = new ChannelInfo(oldName, oldTopic, oldRateLimitPerUser, oldNsfw, oldBitrate, oldType);
var after = new ChannelInfo(newName, newTopic, newRateLimitPerUser, newNsfw, newBitrate, newType);

return new ChannelUpdateAuditLogData(entry.TargetId.Value, before, after);
}


+ 10
- 4
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs View File

@@ -19,8 +19,14 @@ namespace Discord.Rest

internal static MessagePinAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
return new MessagePinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, RestUser.Create(discord, userInfo));
RestUser user = null;
if (entry.TargetId.HasValue)
{
var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
user = RestUser.Create(discord, userInfo);
}

return new MessagePinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, user);
}

/// <summary>
@@ -38,10 +44,10 @@ namespace Discord.Rest
/// </returns>
public ulong ChannelId { get; }
/// <summary>
/// Gets the user of the message that was pinned.
/// Gets the user of the message that was pinned if available.
/// </summary>
/// <returns>
/// A user object representing the user that created the pinned message.
/// A user object representing the user that created the pinned message or <see langword="null"/>.
/// </returns>
public IUser Target { get; }
}


+ 10
- 4
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs View File

@@ -19,8 +19,14 @@ namespace Discord.Rest

internal static MessageUnpinAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
return new MessageUnpinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, RestUser.Create(discord, userInfo));
RestUser user = null;
if (entry.TargetId.HasValue)
{
var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
user = RestUser.Create(discord, userInfo);
}

return new MessageUnpinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, user);
}

/// <summary>
@@ -38,10 +44,10 @@ namespace Discord.Rest
/// </returns>
public ulong ChannelId { get; }
/// <summary>
/// Gets the user of the message that was unpinned.
/// Gets the user of the message that was unpinned if available.
/// </summary>
/// <returns>
/// A user object representing the user that created the unpinned message.
/// A user object representing the user that created the unpinned message or <see langword="null"/>.
/// </returns>
public IUser Target { get; }
}


+ 1
- 1
src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs View File

@@ -22,7 +22,7 @@ namespace Discord.Rest

internal static RestAuditLogEntry Create(BaseDiscordClient discord, Model fullLog, EntryModel model)
{
var userInfo = fullLog.Users.FirstOrDefault(x => x.Id == model.UserId);
var userInfo = model.UserId != null ? fullLog.Users.FirstOrDefault(x => x.Id == model.UserId) : null;
IUser user = null;
if (userInfo != null)
user = RestUser.Create(discord, userInfo);


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

@@ -33,8 +33,8 @@ namespace Discord.Rest
{
TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue,
Deny = overwrite.Permissions.DenyValue
Allow = overwrite.Permissions.AllowValue.ToString(),
Deny = overwrite.Permissions.DenyValue.ToString()
}).ToArray()
: Optional.Create<API.Overwrite[]>(),
};
@@ -59,8 +59,8 @@ namespace Discord.Rest
{
TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue,
Deny = overwrite.Permissions.DenyValue
Allow = overwrite.Permissions.AllowValue.ToString(),
Deny = overwrite.Permissions.DenyValue.ToString()
}).ToArray()
: Optional.Create<API.Overwrite[]>(),
};
@@ -84,8 +84,8 @@ namespace Discord.Rest
{
TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue,
Deny = overwrite.Permissions.DenyValue
Allow = overwrite.Permissions.AllowValue.ToString(),
Deny = overwrite.Permissions.DenyValue.ToString()
}).ToArray()
: Optional.Create<API.Overwrite[]>(),
};
@@ -286,6 +286,13 @@ namespace Discord.Rest
return RestUserMessage.Create(client, channel, client.CurrentUser, model);
}

public static async Task<RestUserMessage> ModifyMessageAsync(IMessageChannel channel, ulong messageId, Action<MessageProperties> func,
BaseDiscordClient client, RequestOptions options)
{
var msgModel = await MessageHelper.ModifyAsync(channel.Id, messageId, client, func, options).ConfigureAwait(false);
return RestUserMessage.Create(client, channel, msgModel.Author.IsSpecified ? RestUser.Create(client, msgModel.Author.Value) : client.CurrentUser, msgModel);
}

public static Task DeleteMessageAsync(IMessageChannel channel, ulong messageId, BaseDiscordClient client,
RequestOptions options)
=> MessageHelper.DeleteAsync(channel.Id, messageId, client, options);
@@ -321,13 +328,13 @@ namespace Discord.Rest
public static async Task AddPermissionOverwriteAsync(IGuildChannel channel, BaseDiscordClient client,
IUser user, OverwritePermissions perms, RequestOptions options)
{
var args = new ModifyChannelPermissionsParams("member", perms.AllowValue, perms.DenyValue);
var args = new ModifyChannelPermissionsParams((int)PermissionTarget.User, perms.AllowValue.ToString(), perms.DenyValue.ToString());
await client.ApiClient.ModifyChannelPermissionsAsync(channel.Id, user.Id, args, options).ConfigureAwait(false);
}
public static async Task AddPermissionOverwriteAsync(IGuildChannel channel, BaseDiscordClient client,
IRole role, OverwritePermissions perms, RequestOptions options)
{
var args = new ModifyChannelPermissionsParams("role", perms.AllowValue, perms.DenyValue);
var args = new ModifyChannelPermissionsParams((int)PermissionTarget.Role, perms.AllowValue.ToString(), perms.DenyValue.ToString());
await client.ApiClient.ModifyChannelPermissionsAsync(channel.Id, role.Id, args, options).ConfigureAwait(false);
}
public static async Task RemovePermissionOverwriteAsync(IGuildChannel channel, BaseDiscordClient client,
@@ -443,8 +450,8 @@ namespace Discord.Rest
{
TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue,
Deny = overwrite.Permissions.DenyValue
Allow = overwrite.Permissions.AllowValue.ToString(),
Deny = overwrite.Permissions.DenyValue.ToString()
}).ToArray()
};
await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false);


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

@@ -135,6 +135,10 @@ namespace Discord.Rest
public Task DeleteMessageAsync(IMessage message, RequestOptions options = null)
=> ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options);

/// <inheritdoc />
public async Task<IUserMessage> ModifyMessageAsync(ulong messageId, Action<MessageProperties> func, RequestOptions options = null)
=> await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false);

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


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

@@ -93,6 +93,10 @@ namespace Discord.Rest
public Task DeleteMessageAsync(IMessage message, RequestOptions options = null)
=> ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options);

/// <inheritdoc />
public async Task<IUserMessage> ModifyMessageAsync(ulong messageId, Action<MessageProperties> func, RequestOptions options = null)
=> await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false);

/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null)


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

@@ -152,6 +152,10 @@ namespace Discord.Rest
public Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null)
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options);

/// <inheritdoc />
public async Task<IUserMessage> ModifyMessageAsync(ulong messageId, Action<MessageProperties> func, RequestOptions options = null)
=> await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false);

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


+ 13
- 28
src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs View File

@@ -4,7 +4,6 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using EmbedModel = Discord.API.GuildEmbed;
using WidgetModel = Discord.API.GuildWidget;
using Model = Discord.API.Guild;
using RoleModel = Discord.API.Role;
@@ -81,26 +80,6 @@ namespace Discord.Rest
return await client.ApiClient.ModifyGuildAsync(guild.Id, apiArgs, options).ConfigureAwait(false);
}
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <c>null</c>.</exception>
public static async Task<EmbedModel> ModifyEmbedAsync(IGuild guild, BaseDiscordClient client,
Action<GuildEmbedProperties> func, RequestOptions options)
{
if (func == null) throw new ArgumentNullException(nameof(func));

var args = new GuildEmbedProperties();
func(args);
var apiArgs = new API.Rest.ModifyGuildEmbedParams
{
Enabled = args.Enabled
};

if (args.Channel.IsSpecified)
apiArgs.ChannelId = args.Channel.Value?.Id;
else if (args.ChannelId.IsSpecified)
apiArgs.ChannelId = args.ChannelId.Value;

return await client.ApiClient.ModifyGuildEmbedAsync(guild.Id, apiArgs, options).ConfigureAwait(false);
}
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <c>null</c>.</exception>
public static async Task<WidgetModel> ModifyWidgetAsync(IGuild guild, BaseDiscordClient client,
Action<GuildWidgetProperties> func, RequestOptions options)
{
@@ -205,8 +184,8 @@ namespace Discord.Rest
{
TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue,
Deny = overwrite.Permissions.DenyValue
Allow = overwrite.Permissions.AllowValue.ToString(),
Deny = overwrite.Permissions.DenyValue.ToString()
}).ToArray()
: Optional.Create<API.Overwrite[]>(),
};
@@ -233,8 +212,8 @@ namespace Discord.Rest
{
TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue,
Deny = overwrite.Permissions.DenyValue
Allow = overwrite.Permissions.AllowValue.ToString(),
Deny = overwrite.Permissions.DenyValue.ToString()
}).ToArray()
: Optional.Create<API.Overwrite[]>(),
};
@@ -258,8 +237,8 @@ namespace Discord.Rest
{
TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue,
Deny = overwrite.Permissions.DenyValue
Allow = overwrite.Permissions.AllowValue.ToString(),
Deny = overwrite.Permissions.DenyValue.ToString()
}).ToArray()
: Optional.Create<API.Overwrite[]>(),
};
@@ -304,6 +283,7 @@ namespace Discord.Rest
var vanityModel = await client.ApiClient.GetVanityInviteAsync(guild.Id, options).ConfigureAwait(false);
if (vanityModel == null) throw new InvalidOperationException("This guild does not have a vanity URL.");
var inviteModel = await client.ApiClient.GetInviteAsync(vanityModel.Code, options).ConfigureAwait(false);
inviteModel.Uses = vanityModel.Uses;
return RestInviteMetadata.Create(client, guild, null, inviteModel);
}

@@ -320,7 +300,7 @@ namespace Discord.Rest
Hoist = isHoisted,
Mentionable = isMentionable,
Name = name,
Permissions = permissions?.RawValue ?? Optional.Create<ulong>()
Permissions = permissions?.RawValue.ToString() ?? Optional.Create<string>()
};

var model = await client.ApiClient.CreateGuildRoleAsync(guild.Id, createGuildRoleParams, options).ConfigureAwait(false);
@@ -496,6 +476,11 @@ namespace Discord.Rest
}

//Emotes
public static async Task<IReadOnlyCollection<GuildEmote>> GetEmotesAsync(IGuild guild, BaseDiscordClient client, RequestOptions options)
{
var models = await client.ApiClient.GetGuildEmotesAsync(guild.Id, options).ConfigureAwait(false);
return models.Select(x => x.ToEntity()).ToImmutableArray();
}
public static async Task<GuildEmote> GetEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options)
{
var emote = await client.ApiClient.GetGuildEmoteAsync(guild.Id, id, options).ConfigureAwait(false);


+ 3
- 51
src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs View File

@@ -6,7 +6,6 @@ using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using EmbedModel = Discord.API.GuildEmbed;
using WidgetModel = Discord.API.GuildWidget;
using Model = Discord.API.Guild;

@@ -27,8 +26,6 @@ namespace Discord.Rest
/// <inheritdoc />
public int AFKTimeout { get; private set; }
/// <inheritdoc />
public bool IsEmbeddable { get; private set; }
/// <inheritdoc />
public bool IsWidgetEnabled { get; private set; }
/// <inheritdoc />
public VerificationLevel VerificationLevel { get; private set; }
@@ -42,8 +39,6 @@ namespace Discord.Rest
/// <inheritdoc />
public ulong? AFKChannelId { get; private set; }
/// <inheritdoc />
public ulong? EmbedChannelId { get; private set; }
/// <inheritdoc />
public ulong? WidgetChannelId { get; private set; }
/// <inheritdoc />
public ulong? SystemChannelId { get; private set; }
@@ -95,8 +90,6 @@ namespace Discord.Rest
/// <inheritdoc />
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);

[Obsolete("DefaultChannelId is deprecated, use GetDefaultChannelAsync")]
public ulong DefaultChannelId => Id;
/// <inheritdoc />
public string IconUrl => CDN.GetGuildIconUrl(Id, IconId);
/// <inheritdoc />
@@ -133,16 +126,12 @@ namespace Discord.Rest
internal void Update(Model model)
{
AFKChannelId = model.AFKChannelId;
if (model.EmbedChannelId.IsSpecified)
EmbedChannelId = model.EmbedChannelId.Value;
if (model.WidgetChannelId.IsSpecified)
WidgetChannelId = model.WidgetChannelId.Value;
SystemChannelId = model.SystemChannelId;
RulesChannelId = model.RulesChannelId;
PublicUpdatesChannelId = model.PublicUpdatesChannelId;
AFKTimeout = model.AFKTimeout;
if (model.EmbedEnabled.IsSpecified)
IsEmbeddable = model.EmbedEnabled.Value;
if (model.WidgetEnabled.IsSpecified)
IsWidgetEnabled = model.WidgetEnabled.Value;
IconId = model.Icon;
@@ -200,11 +189,6 @@ namespace Discord.Rest

Available = true;
}
internal void Update(EmbedModel model)
{
EmbedChannelId = model.ChannelId;
IsEmbeddable = model.Enabled;
}
internal void Update(WidgetModel model)
{
WidgetChannelId = model.ChannelId;
@@ -241,15 +225,6 @@ namespace Discord.Rest
Update(model);
}

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

/// <inheritdoc />
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <see langword="null"/>.</exception>
public async Task ModifyWidgetAsync(Action<GuildWidgetProperties> func, RequestOptions options = null)
@@ -463,23 +438,6 @@ namespace Discord.Rest
.FirstOrDefault();
}

/// <summary>
/// Gets the embed channel (i.e. the channel set in the guild's widget settings) in this guild.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the embed channel set
/// within the server's widget settings; <see langword="null"/> if none is set.
/// </returns>
[Obsolete("This endpoint is deprecated, use GetWidgetChannelAsync instead.")]
public async Task<RestGuildChannel> GetEmbedChannelAsync(RequestOptions options = null)
{
var embedId = EmbedChannelId;
if (embedId.HasValue)
return await GuildHelper.GetChannelAsync(this, Discord, embedId.Value, options).ConfigureAwait(false);
return null;
}

/// <summary>
/// Gets the widget channel (i.e. the channel set in the guild's widget settings) in this guild.
/// </summary>
@@ -828,6 +786,9 @@ namespace Discord.Rest

//Emotes
/// <inheritdoc />
public Task<IReadOnlyCollection<GuildEmote>> GetEmotesAsync(RequestOptions options = null)
=> GuildHelper.GetEmotesAsync(this, Discord, options);
/// <inheritdoc />
public Task<GuildEmote> GetEmoteAsync(ulong id, RequestOptions options = null)
=> GuildHelper.GetEmoteAsync(this, Discord, id, options);
/// <inheritdoc />
@@ -934,15 +895,6 @@ namespace Discord.Rest
return null;
}
/// <inheritdoc />
[Obsolete("This endpoint is deprecated, use GetWidgetChannelAsync instead.")]
async Task<IGuildChannel> IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
return await GetEmbedChannelAsync(options).ConfigureAwait(false);
else
return null;
}
/// <inheritdoc />
async Task<IGuildChannel> IGuild.GetWidgetChannelAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)


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

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

namespace Discord.Rest
{
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public struct RestGuildEmbed
{
public bool IsEnabled { get; private set; }
public ulong? ChannelId { get; private set; }

internal RestGuildEmbed(bool isEnabled, ulong? channelId)
{
ChannelId = channelId;
IsEnabled = isEnabled;
}
internal static RestGuildEmbed Create(Model model)
{
return new RestGuildEmbed(model.Enabled, model.ChannelId);
}

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

+ 9
- 0
src/Discord.Net.Rest/Entities/Invites/RestInvite.cs View File

@@ -21,6 +21,12 @@ namespace Discord.Rest
public ulong ChannelId { get; private set; }
/// <inheritdoc />
public ulong? GuildId { get; private set; }
/// <inheritdoc />
public IUser Inviter { get; private set; }
/// <inheritdoc />
public IUser TargetUser { get; private set; }
/// <inheritdoc />
public TargetUserType TargetUserType { get; private set; }
internal IChannel Channel { get; }
internal IGuild Guild { get; }

@@ -50,6 +56,9 @@ namespace Discord.Rest
MemberCount = model.MemberCount.IsSpecified ? model.MemberCount.Value : null;
PresenceCount = model.PresenceCount.IsSpecified ? model.PresenceCount.Value : null;
ChannelType = (ChannelType)model.Channel.Type;
Inviter = model.Inviter.IsSpecified ? RestUser.Create(Discord, model.Inviter.Value) : null;
TargetUser = model.TargetUser.IsSpecified ? RestUser.Create(Discord, model.TargetUser.Value) : null;
TargetUserType = model.TargetUserType.IsSpecified ? model.TargetUserType.Value : TargetUserType.Undefined;
}

/// <inheritdoc />


+ 5
- 16
src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs View File

@@ -6,10 +6,8 @@ namespace Discord.Rest
/// <summary> Represents additional information regarding the REST-based invite object. </summary>
public class RestInviteMetadata : RestInvite, IInviteMetadata
{
private long? _createdAtTicks;
private long _createdAtTicks;

/// <inheritdoc />
public bool IsRevoked { get; private set; }
/// <inheritdoc />
public bool IsTemporary { get; private set; }
/// <inheritdoc />
@@ -18,10 +16,6 @@ namespace Discord.Rest
public int? MaxUses { get; private set; }
/// <inheritdoc />
public int? Uses { get; private set; }
/// <summary>
/// Gets the user that created this invite.
/// </summary>
public RestUser Inviter { get; private set; }

/// <inheritdoc />
public DateTimeOffset? CreatedAt => DateTimeUtils.FromTicks(_createdAtTicks);
@@ -39,16 +33,11 @@ namespace Discord.Rest
internal void Update(Model model)
{
base.Update(model);
Inviter = model.Inviter != null ? RestUser.Create(Discord, model.Inviter) : null;
IsRevoked = model.Revoked;
IsTemporary = model.Temporary;
MaxAge = model.MaxAge.IsSpecified ? model.MaxAge.Value : (int?)null;
MaxUses = model.MaxUses.IsSpecified ? model.MaxUses.Value : (int?)null;
Uses = model.Uses.IsSpecified ? model.Uses.Value : (int?)null;
_createdAtTicks = model.CreatedAt.IsSpecified ? model.CreatedAt.Value.UtcTicks : (long?)null;
MaxAge = model.MaxAge;
MaxUses = model.MaxUses;
Uses = model.Uses;
_createdAtTicks = model.CreatedAt.UtcTicks;
}

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

+ 92
- 9
src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs View File

@@ -27,26 +27,93 @@ namespace Discord.Rest
public static async Task<Model> ModifyAsync(IMessage msg, BaseDiscordClient client, Action<MessageProperties> func,
RequestOptions options)
{
if (msg.Author.Id != client.CurrentUser.Id)
throw new InvalidOperationException("Only the author of a message may modify the message.");

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

if (msg.Author.Id != client.CurrentUser.Id && (args.Content.IsSpecified || args.Embed.IsSpecified || args.AllowedMentions.IsSpecified))
throw new InvalidOperationException("Only the author of a message may modify the message content, embed, or allowed mentions.");

bool hasText = args.Content.IsSpecified ? !string.IsNullOrEmpty(args.Content.Value) : !string.IsNullOrEmpty(msg.Content);
bool hasEmbed = args.Embed.IsSpecified ? args.Embed.Value != null : msg.Embeds.Any();
if (!hasText && !hasEmbed)
Preconditions.NotNullOrEmpty(args.Content.IsSpecified ? args.Content.Value : string.Empty, nameof(args.Content));

if (args.AllowedMentions.IsSpecified)
{
AllowedMentions allowedMentions = args.AllowedMentions.Value;
Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed.");
Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed.");

// check that user flag and user Id list are exclusive, same with role flag and role Id list
if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue)
{
if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) &&
allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0)
{
throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions));
}

if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) &&
allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0)
{
throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions));
}
}
}

var apiArgs = new API.Rest.ModifyMessageParams
{
Content = args.Content,
Embed = args.Embed.IsSpecified ? args.Embed.Value.ToModel() : Optional.Create<API.Embed>(),
Components = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional<API.ActionRowComponent[]>.Unspecified
Flags = args.Flags.IsSpecified ? args.Flags.Value : Optional.Create<MessageFlags?>(),
AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value.ToModel() : Optional.Create<API.AllowedMentions>(),
};
return await client.ApiClient.ModifyMessageAsync(msg.Channel.Id, msg.Id, apiArgs, options).ConfigureAwait(false);
}

public static async Task<Model> ModifyAsync(ulong channelId, ulong msgId, BaseDiscordClient client, Action<MessageProperties> func,
RequestOptions options)
{
var args = new MessageProperties();
func(args);

if ((args.Content.IsSpecified && string.IsNullOrEmpty(args.Content.Value)) && (args.Embed.IsSpecified && args.Embed.Value == null))
Preconditions.NotNullOrEmpty(args.Content.IsSpecified ? args.Content.Value : string.Empty, nameof(args.Content));

if (args.AllowedMentions.IsSpecified)
{
AllowedMentions allowedMentions = args.AllowedMentions.Value;
Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed.");
Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed.");

// check that user flag and user Id list are exclusive, same with role flag and role Id list
if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue)
{
if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) &&
allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0)
{
throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions));
}

if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) &&
allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0)
{
throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions));
}
}
}

var apiArgs = new API.Rest.ModifyMessageParams
{
Content = args.Content,
Embed = args.Embed.IsSpecified ? args.Embed.Value.ToModel() : Optional.Create<API.Embed>(),
Flags = args.Flags.IsSpecified ? args.Flags.Value : Optional.Create<MessageFlags?>(),
AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value.ToModel() : Optional.Create<API.AllowedMentions>(),
};
return await client.ApiClient.ModifyMessageAsync(channelId, msgId, apiArgs, options).ConfigureAwait(false);
}

public static Task DeleteAsync(IMessage msg, BaseDiscordClient client, RequestOptions options)
=> DeleteAsync(msg.Channel.Id, msg.Id, client, options);

@@ -56,13 +123,14 @@ namespace Discord.Rest
await client.ApiClient.DeleteMessageAsync(channelId, msgId, options).ConfigureAwait(false);
}

public static async Task SuppressEmbedsAsync(IMessage msg, BaseDiscordClient client, bool suppress, RequestOptions options)
public static async Task AddReactionAsync(ulong channelId, ulong messageId, IEmote emote, BaseDiscordClient client, RequestOptions options)
{
var apiArgs = new API.Rest.SuppressEmbedParams
{
Suppressed = suppress
};
await client.ApiClient.SuppressEmbedAsync(msg.Channel.Id, msg.Id, apiArgs, options).ConfigureAwait(false);
await client.ApiClient.AddReactionAsync(channelId, messageId, emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name), options).ConfigureAwait(false);
}

public static async Task AddReactionAsync(ulong channelId, ulong messageId, IEmote emote, BaseDiscordClient client, RequestOptions options)
{
await client.ApiClient.AddReactionAsync(channelId, messageId, emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name), options).ConfigureAwait(false);
}

public static async Task AddReactionAsync(IMessage msg, IEmote emote, BaseDiscordClient client, RequestOptions options)
@@ -70,16 +138,31 @@ namespace Discord.Rest
await client.ApiClient.AddReactionAsync(msg.Channel.Id, msg.Id, emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name), options).ConfigureAwait(false);
}

public static async Task RemoveReactionAsync(ulong channelId, ulong messageId, ulong userId, IEmote emote, BaseDiscordClient client, RequestOptions options)
{
await client.ApiClient.RemoveReactionAsync(channelId, messageId, userId, emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name), options).ConfigureAwait(false);
}

public static async Task RemoveReactionAsync(IMessage msg, ulong userId, IEmote emote, BaseDiscordClient client, RequestOptions options)
{
await client.ApiClient.RemoveReactionAsync(msg.Channel.Id, msg.Id, userId, emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name), options).ConfigureAwait(false);
}

public static async Task RemoveAllReactionsAsync(ulong channelId, ulong messageId, BaseDiscordClient client, RequestOptions options)
{
await client.ApiClient.RemoveAllReactionsAsync(channelId, messageId, options).ConfigureAwait(false);
}

public static async Task RemoveAllReactionsAsync(IMessage msg, BaseDiscordClient client, RequestOptions options)
{
await client.ApiClient.RemoveAllReactionsAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false);
}

public static async Task RemoveAllReactionsForEmoteAsync(ulong channelId, ulong messageId, IEmote emote, BaseDiscordClient client, RequestOptions options)
{
await client.ApiClient.RemoveAllReactionsForEmoteAsync(channelId, messageId, emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name), options).ConfigureAwait(false);
}

public static async Task RemoveAllReactionsForEmoteAsync(IMessage msg, IEmote emote, BaseDiscordClient client, RequestOptions options)
{
await client.ApiClient.RemoveAllReactionsForEmoteAsync(msg.Channel.Id, msg.Id, emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name), options).ConfigureAwait(false);


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

@@ -58,6 +58,8 @@ namespace Discord.Rest
public virtual IReadOnlyCollection<RestUser> MentionedUsers => ImmutableArray.Create<RestUser>();
/// <inheritdoc />
public virtual IReadOnlyCollection<ITag> Tags => ImmutableArray.Create<ITag>();
/// <inheritdoc />
public virtual IReadOnlyCollection<Sticker> Stickers => ImmutableArray.Create<Sticker>();

/// <inheritdoc />
public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks);
@@ -67,6 +69,8 @@ namespace Discord.Rest
public MessageApplication Application { get; private set; }
/// <inheritdoc />
public MessageReference Reference { get; private set; }
/// <inheritdoc />
public MessageFlags? Flags { get; private set; }

/// <inheritdoc/>
public IReadOnlyCollection<ActionRowComponent> Components { get; private set; }
@@ -143,6 +147,9 @@ namespace Discord.Rest
else
Components = new List<ActionRowComponent>();

if (model.Flags.IsSpecified)
Flags = model.Flags.Value;

if (model.Reactions.IsSpecified)
{
var value = model.Reactions.Value;
@@ -187,8 +194,13 @@ namespace Discord.Rest
IReadOnlyCollection<IEmbed> IMessage.Embeds => Embeds;
/// <inheritdoc />
IReadOnlyCollection<ulong> IMessage.MentionedUserIds => MentionedUsers.Select(x => x.Id).ToImmutableArray();
/// <inheritdoc/>
IReadOnlyCollection<IMessageComponent> IMessage.Components => Components;

/// <inheritdoc />
IReadOnlyCollection<ISticker> IMessage.Stickers => Stickers;

/// <inheritdoc />
public IReadOnlyDictionary<IEmote, ReactionMetadata> Reactions => _reactions.ToDictionary(x => x.Emote, x => new ReactionMetadata { ReactionCount = x.Count, IsMe = x.Me });



+ 19
- 9
src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs View File

@@ -13,7 +13,7 @@ namespace Discord.Rest
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class RestUserMessage : RestMessage, IUserMessage
{
private bool _isMentioningEveryone, _isTTS, _isPinned, _isSuppressed;
private bool _isMentioningEveryone, _isTTS, _isPinned;
private long? _editedTimestampTicks;
private IUserMessage _referencedMessage;
private ImmutableArray<Attachment> _attachments = ImmutableArray.Create<Attachment>();
@@ -21,13 +21,14 @@ namespace Discord.Rest
private ImmutableArray<ITag> _tags = ImmutableArray.Create<ITag>();
private ImmutableArray<ulong> _roleMentionIds = ImmutableArray.Create<ulong>();
private ImmutableArray<RestUser> _userMentions = ImmutableArray.Create<RestUser>();
private ImmutableArray<Sticker> _stickers = ImmutableArray.Create<Sticker>();

/// <inheritdoc />
public override bool IsTTS => _isTTS;
/// <inheritdoc />
public override bool IsPinned => _isPinned;
/// <inheritdoc />
public override bool IsSuppressed => _isSuppressed;
public override bool IsSuppressed => Flags.HasValue && Flags.Value.HasFlag(MessageFlags.SuppressEmbeds);
/// <inheritdoc />
public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks);
/// <inheritdoc />
@@ -45,6 +46,8 @@ namespace Discord.Rest
/// <inheritdoc />
public override IReadOnlyCollection<ITag> Tags => _tags;
/// <inheritdoc />
public override IReadOnlyCollection<Sticker> Stickers => _stickers;
/// <inheritdoc />
public IUserMessage ReferencedMessage => _referencedMessage;

internal RestUserMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source)
@@ -70,10 +73,6 @@ namespace Discord.Rest
_editedTimestampTicks = model.EditedTimestamp.Value?.UtcTicks;
if (model.MentionEveryone.IsSpecified)
_isMentioningEveryone = model.MentionEveryone.Value;
if (model.Flags.IsSpecified)
{
_isSuppressed = model.Flags.Value.HasFlag(API.MessageFlags.Suppressed);
}
if (model.RoleMentions.IsSpecified)
_roleMentionIds = model.RoleMentions.Value.ToImmutableArray();

@@ -136,6 +135,20 @@ namespace Discord.Rest
IUser refMsgAuthor = MessageHelper.GetAuthor(Discord, guild, refMsg.Author.Value, refMsg.WebhookId.ToNullable());
_referencedMessage = RestUserMessage.Create(Discord, Channel, refMsgAuthor, refMsg);
}

if (model.Stickers.IsSpecified)
{
var value = model.Stickers.Value;
if (value.Length > 0)
{
var stickers = ImmutableArray.CreateBuilder<Sticker>(value.Length);
for (int i = 0; i < value.Length; i++)
stickers.Add(Sticker.Create(value[i]));
_stickers = stickers.ToImmutable();
}
else
_stickers = ImmutableArray.Create<Sticker>();
}
}

/// <inheritdoc />
@@ -151,9 +164,6 @@ namespace Discord.Rest
/// <inheritdoc />
public Task UnpinAsync(RequestOptions options = null)
=> MessageHelper.UnpinAsync(this, Discord, options);
/// <inheritdoc />
public Task ModifySuppressionAsync(bool suppressEmbeds, RequestOptions options = null)
=> MessageHelper.SuppressEmbedsAsync(this, Discord, suppressEmbeds, options);

public string Resolve(int startIndex, TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name,
TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name)


+ 48
- 0
src/Discord.Net.Rest/Entities/Messages/Sticker.cs View File

@@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.Diagnostics;
using Model = Discord.API.Sticker;

namespace Discord
{
/// <inheritdoc cref="ISticker"/>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class Sticker : ISticker
{
/// <inheritdoc />
public ulong Id { get; }
/// <inheritdoc />
public ulong PackId { get; }
/// <inheritdoc />
public string Name { get; }
/// <inheritdoc />
public string Description { get; }
/// <inheritdoc />
public IReadOnlyCollection<string> Tags { get; }
/// <inheritdoc />
public string Asset { get; }
/// <inheritdoc />
public string PreviewAsset { get; }
/// <inheritdoc />
public StickerFormatType FormatType { get; }

internal Sticker(ulong id, ulong packId, string name, string description, string[] tags, string asset, string previewAsset, StickerFormatType formatType)
{
Id = id;
PackId = packId;
Name = name;
Description = description;
Tags = tags.ToReadOnlyCollection();
Asset = asset;
PreviewAsset = previewAsset;
FormatType = formatType;
}
internal static Sticker Create(Model model)
{
return new Sticker(model.Id, model.PackId, model.Name, model.Desription,
model.Tags.IsSpecified ? model.Tags.Value.Split(',') : new string[0],
model.Asset, model.PreviewAsset, model.FormatType);
}

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

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

@@ -26,6 +26,8 @@ namespace Discord.Rest
public GuildPermissions Permissions { get; private set; }
/// <inheritdoc />
public int Position { get; private set; }
/// <inheritdoc />
public RoleTags Tags { get; private set; }

/// <inheritdoc />
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);
@@ -56,6 +58,8 @@ namespace Discord.Rest
Position = model.Position;
Color = new Color(model.Color);
Permissions = new GuildPermissions(model.Permissions);
if (model.Tags.IsSpecified)
Tags = model.Tags.Value.ToEntity();
}

/// <inheritdoc />


+ 2
- 2
src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading.Tasks;
using Model = Discord.API.Role;
using BulkParams = Discord.API.Rest.ModifyGuildRolesParams;
@@ -24,7 +24,7 @@ namespace Discord.Rest
Hoist = args.Hoist,
Mentionable = args.Mentionable,
Name = args.Name,
Permissions = args.Permissions.IsSpecified ? args.Permissions.Value.RawValue : Optional.Create<ulong>()
Permissions = args.Permissions.IsSpecified ? args.Permissions.Value.RawValue.ToString() : Optional.Create<string>()
};
var model = await client.ApiClient.ModifyGuildRoleAsync(role.Guild.Id, role.Id, apiArgs, options).ConfigureAwait(false);



+ 20
- 4
src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs View File

@@ -29,6 +29,8 @@ namespace Discord.Rest
public DateTimeOffset? PremiumSince => DateTimeUtils.FromTicks(_premiumSinceTicks);
/// <inheritdoc />
public ulong GuildId => Guild.Id;
/// <inheritdoc />
public bool? IsPending { get; private set; }

/// <inheritdoc />
/// <exception cref="InvalidOperationException" accessor="get">Resolving permissions requires the parent guild to be downloaded.</exception>
@@ -73,6 +75,8 @@ namespace Discord.Rest
UpdateRoles(model.Roles.Value);
if (model.PremiumSince.IsSpecified)
_premiumSinceTicks = model.PremiumSince.Value?.UtcTicks;
if (model.Pending.IsSpecified)
IsPending = model.Pending.Value;
}
private void UpdateRoles(ulong[] roleIds)
{
@@ -108,17 +112,29 @@ namespace Discord.Rest
public Task KickAsync(string reason = null, RequestOptions options = null)
=> UserHelper.KickAsync(this, Discord, reason, options);
/// <inheritdoc />
public Task AddRoleAsync(ulong roleId, RequestOptions options = null)
=> AddRolesAsync(new[] { roleId }, options);
/// <inheritdoc />
public Task AddRoleAsync(IRole role, RequestOptions options = null)
=> AddRolesAsync(new[] { role }, options);
=> AddRoleAsync(role.Id, options);
/// <inheritdoc />
public Task AddRolesAsync(IEnumerable<ulong> roleIds, RequestOptions options = null)
=> UserHelper.AddRolesAsync(this, Discord, roleIds, options);
/// <inheritdoc />
public Task AddRolesAsync(IEnumerable<IRole> roles, RequestOptions options = null)
=> UserHelper.AddRolesAsync(this, Discord, roles, options);
=> AddRolesAsync(roles.Select(x => x.Id), options);
/// <inheritdoc />
public Task RemoveRoleAsync(ulong roleId, RequestOptions options = null)
=> RemoveRolesAsync(new[] { roleId }, options);
/// <inheritdoc />
public Task RemoveRoleAsync(IRole role, RequestOptions options = null)
=> RemoveRolesAsync(new[] { role }, options);
=> RemoveRoleAsync(role.Id, options);
/// <inheritdoc />
public Task RemoveRolesAsync(IEnumerable<ulong> roleIds, RequestOptions options = null)
=> UserHelper.RemoveRolesAsync(this, Discord, roleIds, options);
/// <inheritdoc />
public Task RemoveRolesAsync(IEnumerable<IRole> roles, RequestOptions options = null)
=> UserHelper.RemoveRolesAsync(this, Discord, roles, options);
=> RemoveRolesAsync(roles.Select(x => x.Id));

/// <inheritdoc />
/// <exception cref="InvalidOperationException">Resolving permissions requires the parent guild to be downloaded.</exception>


+ 8
- 4
src/Discord.Net.Rest/Entities/Users/RestUser.cs View File

@@ -21,6 +21,8 @@ namespace Discord.Rest
public ushort DiscriminatorValue { get; private set; }
/// <inheritdoc />
public string AvatarId { get; private set; }
/// <inheritdoc />
public UserProperties? PublicFlags { get; private set; }

/// <inheritdoc />
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);
@@ -65,6 +67,8 @@ namespace Discord.Rest
IsBot = model.Bot.Value;
if (model.Username.IsSpecified)
Username = model.Username.Value;
if (model.PublicFlags.IsSpecified)
PublicFlags = model.PublicFlags.Value;
}

/// <inheritdoc />
@@ -75,13 +79,13 @@ namespace Discord.Rest
}

/// <summary>
/// Returns a direct message channel to this user, or create one if it does not already exist.
/// Creates a direct message channel to this user.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a rest DM channel where the user is the recipient.
/// </returns>
public Task<RestDMChannel> GetOrCreateDMChannelAsync(RequestOptions options = null)
public Task<RestDMChannel> CreateDMChannelAsync(RequestOptions options = null)
=> UserHelper.CreateDMChannelAsync(this, Discord, options);

/// <inheritdoc />
@@ -103,7 +107,7 @@ namespace Discord.Rest

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

+ 20
- 10
src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs View File

@@ -52,32 +52,42 @@ namespace Discord.Rest
/// <inheritdoc />
string IGuildUser.Nickname => null;
/// <inheritdoc />
bool? IGuildUser.IsPending => null;
/// <inheritdoc />
GuildPermissions IGuildUser.GuildPermissions => GuildPermissions.Webhook;

/// <inheritdoc />
ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => Permissions.ToChannelPerms(channel, GuildPermissions.Webhook.RawValue);
/// <inheritdoc />
Task IGuildUser.KickAsync(string reason, RequestOptions options) =>
Task IGuildUser.KickAsync(string reason, RequestOptions options) =>
throw new NotSupportedException("Webhook users cannot be kicked.");

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

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

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

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

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

//IVoiceState


+ 6
- 6
src/Discord.Net.Rest/Entities/Users/UserHelper.cs View File

@@ -73,16 +73,16 @@ namespace Discord.Rest
return RestDMChannel.Create(client, await client.ApiClient.CreateDMChannelAsync(args, options).ConfigureAwait(false));
}

public static async Task AddRolesAsync(IGuildUser user, BaseDiscordClient client, IEnumerable<IRole> roles, RequestOptions options)
public static async Task AddRolesAsync(IGuildUser user, BaseDiscordClient client, IEnumerable<ulong> roleIds, RequestOptions options)
{
foreach (var role in roles)
await client.ApiClient.AddRoleAsync(user.Guild.Id, user.Id, role.Id, options).ConfigureAwait(false);
foreach (var roleId in roleIds)
await client.ApiClient.AddRoleAsync(user.Guild.Id, user.Id, roleId, options).ConfigureAwait(false);
}

public static async Task RemoveRolesAsync(IGuildUser user, BaseDiscordClient client, IEnumerable<IRole> roles, RequestOptions options)
public static async Task RemoveRolesAsync(IGuildUser user, BaseDiscordClient client, IEnumerable<ulong> roleIds, RequestOptions options)
{
foreach (var role in roles)
await client.ApiClient.RemoveRoleAsync(user.Guild.Id, user.Id, role.Id, options).ConfigureAwait(false);
foreach (var roleId in roleIds)
await client.ApiClient.RemoveRoleAsync(user.Guild.Id, user.Id, roleId, options).ConfigureAwait(false);
}
}
}

+ 4
- 2
src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs View File

@@ -11,11 +11,11 @@ namespace Discord.Rest
internal IGuild Guild { get; private set; }
internal ITextChannel Channel { get; private set; }

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

/// <inheritdoc />
public ulong ChannelId { get; private set; }
/// <inheritdoc />
public string Name { get; private set; }
/// <inheritdoc />
@@ -56,6 +56,8 @@ namespace Discord.Rest

internal void Update(Model model)
{
if (ChannelId != model.ChannelId)
ChannelId = model.ChannelId;
if (model.Avatar.IsSpecified)
AvatarId = model.Avatar.Value;
if (model.Creator.IsSpecified)


+ 7
- 0
src/Discord.Net.Rest/Extensions/EntityExtensions.cs View File

@@ -34,6 +34,13 @@ namespace Discord.Rest
model.Thumbnail.IsSpecified ? model.Thumbnail.Value.ToEntity() : (EmbedThumbnail?)null,
model.Fields.IsSpecified ? model.Fields.Value.Select(x => x.ToEntity()).ToImmutableArray() : ImmutableArray.Create<EmbedField>());
}
public static RoleTags ToEntity(this API.RoleTags model)
{
return new RoleTags(
model.BotId.IsSpecified ? model.BotId.Value : null,
model.IntegrationId.IsSpecified ? model.IntegrationId.Value : null,
model.IsPremiumSubscriber.IsSpecified ? true : false);
}
public static API.Embed ToModel(this Embed entity)
{
if (entity == null) return null;


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

@@ -73,8 +73,6 @@ namespace Discord.Net.Converters
}

//Enums
if (type == typeof(PermissionTarget))
return PermissionTargetConverter.Instance;
if (type == typeof(UserStatus))
return UserStatusConverter.Instance;
if (type == typeof(EmbedType))


+ 0
- 44
src/Discord.Net.Rest/Net/Converters/PermissionTargetConverter.cs View File

@@ -1,44 +0,0 @@
using Newtonsoft.Json;
using System;

namespace Discord.Net.Converters
{
internal class PermissionTargetConverter : JsonConverter
{
public static readonly PermissionTargetConverter Instance = new PermissionTargetConverter();

public override bool CanConvert(Type objectType) => true;
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)
{
case "member":
return PermissionTarget.User;
case "role":
return PermissionTarget.Role;
default:
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)
{
case PermissionTarget.User:
writer.WriteValue("member");
break;
case PermissionTarget.Role:
writer.WriteValue("role");
break;
default:
throw new JsonSerializationException("Invalid permission target.");
}
}
}
}

+ 0
- 2
src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs View File

@@ -17,8 +17,6 @@ namespace Discord.API.Gateway
public Optional<int[]> ShardingParams { get; set; }
[JsonProperty("presence")]
public Optional<StatusUpdateParams> Presence { get; set; }
[JsonProperty("guild_subscriptions")]
public Optional<bool> GuildSubscriptions { get; set; }
[JsonProperty("intents")]
public Optional<int> Intents { get; set; }
}


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save