Browse Source

Merge branch 'dev' into inline-replies

pull/1659/head
Paulo GitHub 4 years ago
parent
commit
1614782ee1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 637 additions and 46 deletions
  1. +5
    -1
      src/Discord.Net.Core/Audio/IAudioClient.cs
  2. +11
    -0
      src/Discord.Net.Core/CDN.cs
  3. +9
    -0
      src/Discord.Net.Core/Entities/Channels/INewsChannel.cs
  4. +2
    -1
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  5. +12
    -0
      src/Discord.Net.Core/Entities/IApplication.cs
  6. +14
    -0
      src/Discord.Net.Core/Entities/Invites/TargetUserType.cs
  7. +1
    -1
      src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs
  8. +4
    -0
      src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs
  9. +9
    -2
      src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs
  10. +27
    -0
      src/Discord.Net.Core/Entities/Teams/ITeam.cs
  11. +25
    -0
      src/Discord.Net.Core/Entities/Teams/ITeamMember.cs
  12. +11
    -0
      src/Discord.Net.Core/Entities/Teams/MembershipState.cs
  13. +7
    -1
      src/Discord.Net.Rest/API/Common/Application.cs
  14. +2
    -2
      src/Discord.Net.Rest/API/Common/GuildEmbed.cs
  15. +9
    -0
      src/Discord.Net.Rest/API/Common/MembershipState.cs
  16. +17
    -0
      src/Discord.Net.Rest/API/Common/Team.cs
  17. +17
    -0
      src/Discord.Net.Rest/API/Common/TeamMember.cs
  18. +5
    -1
      src/Discord.Net.Rest/API/Rest/GuildPruneParams.cs
  19. +3
    -3
      src/Discord.Net.Rest/BaseDiscordClient.cs
  20. +2
    -1
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  21. +1
    -1
      src/Discord.Net.Rest/Entities/Channels/RestNewsChannel.cs
  22. +5
    -5
      src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
  23. +3
    -3
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  24. +2
    -2
      src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs
  25. +10
    -0
      src/Discord.Net.Rest/Entities/RestApplication.cs
  26. +37
    -0
      src/Discord.Net.Rest/Entities/Teams/RestTeam.cs
  27. +30
    -0
      src/Discord.Net.Rest/Entities/Teams/RestTeamMember.cs
  28. +3
    -3
      src/Discord.Net.Rest/Net/Queue/RequestQueue.cs
  29. +2
    -2
      src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs
  30. +31
    -0
      src/Discord.Net.WebSocket/API/Gateway/InviteCreateEvent.cs
  31. +14
    -0
      src/Discord.Net.WebSocket/API/Gateway/InviteDeleteEvent.cs
  32. +7
    -1
      src/Discord.Net.WebSocket/Audio/AudioClient.cs
  33. +3
    -0
      src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs
  34. +4
    -2
      src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs
  35. +5
    -3
      src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs
  36. +7
    -0
      src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs
  37. +8
    -1
      src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs
  38. +7
    -0
      src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs
  39. +7
    -0
      src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs
  40. +42
    -0
      src/Discord.Net.WebSocket/BaseSocketClient.Events.cs
  41. +3
    -0
      src/Discord.Net.WebSocket/DiscordShardedClient.cs
  42. +59
    -1
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  43. +2
    -1
      src/Discord.Net.WebSocket/DiscordSocketConfig.cs
  44. +1
    -1
      src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs
  45. +2
    -2
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  46. +143
    -0
      src/Discord.Net.WebSocket/Entities/Invites/SocketInvite.cs
  47. +2
    -2
      src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs
  48. +3
    -3
      src/Discord.Net.Webhook/DiscordWebhookClient.cs
  49. +2
    -0
      test/Discord.Net.Tests.Unit/GuildPermissionsTests.cs

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

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

namespace Discord.Audio
@@ -20,6 +21,9 @@ namespace Discord.Audio
/// <summary> Gets the estimated round-trip latency, in milliseconds, to the voice UDP server. </summary>
int UdpLatency { get; }

/// <summary>Gets the current audio streams.</summary>
IReadOnlyDictionary<ulong, AudioInStream> GetStreams();

Task StopAsync();
Task SetSpeakingAsync(bool value);



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

@@ -7,6 +7,17 @@ namespace Discord
/// </summary>
public static class CDN
{
/// <summary>
/// Returns a team icon URL.
/// </summary>
/// <param name="teamId">The team identifier.</param>
/// <param name="iconId">The icon identifier.</param>
/// <returns>
/// A URL pointing to the team's icon.
/// </returns>
public static string GetTeamIconUrl(ulong teamId, string iconId)
=> iconId != null ? $"{DiscordConfig.CDNUrl}team-icons/{teamId}/{iconId}.jpg" : null;

/// <summary>
/// Returns an application icon URL.
/// </summary>


+ 9
- 0
src/Discord.Net.Core/Entities/Channels/INewsChannel.cs View File

@@ -0,0 +1,9 @@
namespace Discord
{
/// <summary>
/// Represents a generic news channel in a guild that can send and receive messages.
/// </summary>
public interface INewsChannel : ITextChannel
{
}
}

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

@@ -705,11 +705,12 @@ namespace Discord
/// <param name="days">The number of days required for the users to be kicked.</param>
/// <param name="simulate">Whether this prune action is a simulation.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <param name="includeRoleIds">An array of role IDs to be included in the prune of users who do not have any additional roles.</param>
/// <returns>
/// A task that represents the asynchronous prune operation. The task result contains the number of users to
/// be or has been removed from this guild.
/// </returns>
Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null);
Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null, IEnumerable<ulong> includeRoleIds = null);
/// <summary>
/// Gets a collection of users in this guild that the name or nickname starts with the
/// provided <see cref="string"/> at <paramref name="query"/>.


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

@@ -22,6 +22,18 @@ namespace Discord
/// Gets the icon URL of the application.
/// </summary>
string IconUrl { get; }
/// <summary>
/// Gets if the bot is public.
/// </summary>
bool IsBotPublic { get; }
/// <summary>
/// Gets if the bot requires code grant.
/// </summary>
bool BotRequiresCodeGrant { get; }
/// <summary>
/// Gets the team associated with this application if there is one.
/// </summary>
ITeam Team { get; }

/// <summary>
/// Gets the partial user object containing info on the owner of the application.


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

@@ -0,0 +1,14 @@
namespace Discord
{
public enum TargetUserType
{
/// <summary>
/// The invite whose target user type is not defined.
/// </summary>
Undefined = 0,
/// <summary>
/// The invite is for a Go Live stream.
/// </summary>
Stream = 1
}
}

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

@@ -17,7 +17,7 @@ namespace Discord
/// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for category channels. </summary>
public static readonly ChannelPermissions Category = new ChannelPermissions(0b01100_1111110_1111111110001_010001);
/// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for direct message channels. </summary>
public static readonly ChannelPermissions DM = new ChannelPermissions(0b00000_1000110_1011100110000_000000);
public static readonly ChannelPermissions DM = new ChannelPermissions(0b00000_1000110_1011100110001_000000);
/// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for group channels. </summary>
public static readonly ChannelPermissions Group = new ChannelPermissions(0b00000_1000110_0001101100000_000000);
/// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for a given channel type. </summary>


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

@@ -51,6 +51,10 @@ namespace Discord
/// authentication when used on a guild that has server-wide 2FA enabled.
/// </remarks>
ManageGuild = 0x00_00_00_20,
/// <summary>
/// Allows for viewing of guild insights
/// </summary>
ViewGuildInsights = 0x00_08_00_00,

// Text
/// <summary>


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

@@ -12,7 +12,7 @@ namespace Discord
/// <summary> Gets a <see cref="GuildPermissions"/> that grants all guild permissions for webhook users. </summary>
public static readonly GuildPermissions Webhook = new GuildPermissions(0b00000_0000000_0001101100000_000000);
/// <summary> Gets a <see cref="GuildPermissions"/> that grants all guild permissions. </summary>
public static readonly GuildPermissions All = new GuildPermissions(0b11111_1111110_1111111111111_111111);
public static readonly GuildPermissions All = new GuildPermissions(0b11111_1111111_1111111111111_111111);

/// <summary> Gets a packed value representing all the permissions in this <see cref="GuildPermissions"/>. </summary>
public ulong RawValue { get; }
@@ -34,6 +34,8 @@ namespace Discord
public bool AddReactions => Permissions.GetValue(RawValue, GuildPermission.AddReactions);
/// <summary> If <c>true</c>, a user may view the audit log. </summary>
public bool ViewAuditLog => Permissions.GetValue(RawValue, GuildPermission.ViewAuditLog);
/// <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.")]
@@ -97,6 +99,7 @@ namespace Discord
bool? manageGuild = null,
bool? addReactions = null,
bool? viewAuditLog = null,
bool? viewGuildInsights = null,
bool? viewChannel = null,
bool? sendMessages = null,
bool? sendTTSMessages = null,
@@ -130,6 +133,7 @@ namespace Discord
Permissions.SetValue(ref value, manageGuild, GuildPermission.ManageGuild);
Permissions.SetValue(ref value, addReactions, GuildPermission.AddReactions);
Permissions.SetValue(ref value, viewAuditLog, GuildPermission.ViewAuditLog);
Permissions.SetValue(ref value, viewGuildInsights, GuildPermission.ViewGuildInsights);
Permissions.SetValue(ref value, viewChannel, GuildPermission.ViewChannel);
Permissions.SetValue(ref value, sendMessages, GuildPermission.SendMessages);
Permissions.SetValue(ref value, sendTTSMessages, GuildPermission.SendTTSMessages);
@@ -166,6 +170,7 @@ namespace Discord
bool manageGuild = false,
bool addReactions = false,
bool viewAuditLog = false,
bool viewGuildInsights = false,
bool viewChannel = false,
bool sendMessages = false,
bool sendTTSMessages = false,
@@ -198,6 +203,7 @@ namespace Discord
manageGuild: manageGuild,
addReactions: addReactions,
viewAuditLog: viewAuditLog,
viewGuildInsights: viewGuildInsights,
viewChannel: viewChannel,
sendMessages: sendMessages,
sendTTSMessages: sendTTSMessages,
@@ -231,6 +237,7 @@ namespace Discord
bool? manageGuild = null,
bool? addReactions = null,
bool? viewAuditLog = null,
bool? viewGuildInsights = null,
bool? viewChannel = null,
bool? sendMessages = null,
bool? sendTTSMessages = null,
@@ -254,7 +261,7 @@ namespace Discord
bool? manageWebhooks = null,
bool? manageEmojis = null)
=> new GuildPermissions(RawValue, createInstantInvite, kickMembers, banMembers, administrator, manageChannels, manageGuild, addReactions,
viewAuditLog, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles,
viewAuditLog, viewGuildInsights, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles,
readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, moveMembers,
useVoiceActivation, prioritySpeaker, stream, changeNickname, manageNicknames, manageRoles, manageWebhooks, manageEmojis);



+ 27
- 0
src/Discord.Net.Core/Entities/Teams/ITeam.cs View File

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

namespace Discord
{
/// <summary>
/// Represents a Discord Team.
/// </summary>
public interface ITeam
{
/// <summary>
/// Gets the team icon url.
/// </summary>
string IconUrl { get; }
/// <summary>
/// Gets the team unique identifier.
/// </summary>
ulong Id { get; }
/// <summary>
/// Gets the members of this team.
/// </summary>
IReadOnlyList<ITeamMember> TeamMembers { get; }
/// <summary>
/// Gets the user identifier that owns this team.
/// </summary>
ulong OwnerUserId { get; }
}
}

+ 25
- 0
src/Discord.Net.Core/Entities/Teams/ITeamMember.cs View File

@@ -0,0 +1,25 @@
namespace Discord
{
/// <summary>
/// Represents a Discord Team member.
/// </summary>
public interface ITeamMember
{
/// <summary>
/// Gets the membership state of this team member.
/// </summary>
MembershipState MembershipState { get; }
/// <summary>
/// Gets the permissions of this team member.
/// </summary>
string[] Permissions { get; }
/// <summary>
/// Gets the team unique identifier for this team member.
/// </summary>
ulong TeamId { get; }
/// <summary>
/// Gets the Discord user of this team member.
/// </summary>
IUser User { get; }
}
}

+ 11
- 0
src/Discord.Net.Core/Entities/Teams/MembershipState.cs View File

@@ -0,0 +1,11 @@
namespace Discord
{
/// <summary>
/// Represents the membership state of a team member.
/// </summary>
public enum MembershipState
{
Invited,
Accepted,
}
}

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

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

namespace Discord.API
@@ -15,6 +15,12 @@ namespace Discord.API
public ulong Id { get; set; }
[JsonProperty("icon")]
public string Icon { get; set; }
[JsonProperty("bot_public")]
public bool IsBotPublic { get; set; }
[JsonProperty("bot_require_code_grant")]
public bool BotRequiresCodeGrant { get; set; }
[JsonProperty("team")]
public Team Team { get; set; }

[JsonProperty("flags"), Int53]
public Optional<ulong> Flags { get; set; }


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

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

namespace Discord.API
@@ -8,6 +8,6 @@ namespace Discord.API
[JsonProperty("enabled")]
public bool Enabled { get; set; }
[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }
public ulong? ChannelId { get; set; }
}
}

+ 9
- 0
src/Discord.Net.Rest/API/Common/MembershipState.cs View File

@@ -0,0 +1,9 @@
namespace Discord.API
{
internal enum MembershipState
{
None = 0,
Invited = 1,
Accepted = 2,
}
}

+ 17
- 0
src/Discord.Net.Rest/API/Common/Team.cs View File

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

namespace Discord.API
{
internal class Team
{
[JsonProperty("icon")]
public Optional<string> Icon { get; set; }
[JsonProperty("id")]
public ulong Id { get; set; }
[JsonProperty("members")]
public TeamMember[] TeamMembers { get; set; }
[JsonProperty("owner_user_id")]
public ulong OwnerUserId { get; set; }
}
}

+ 17
- 0
src/Discord.Net.Rest/API/Common/TeamMember.cs View File

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

namespace Discord.API
{
internal class TeamMember
{
[JsonProperty("membership_state")]
public MembershipState MembershipState { get; set; }
[JsonProperty("permissions")]
public string[] Permissions { get; set; }
[JsonProperty("team_id")]
public ulong TeamId { get; set; }
[JsonProperty("user")]
public User User { get; set; }
}
}

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

@@ -9,9 +9,13 @@ namespace Discord.API.Rest
[JsonProperty("days")]
public int Days { get; }

public GuildPruneParams(int days)
[JsonProperty("include_roles")]
public ulong[] IncludeRoleIds { get; }

public GuildPruneParams(int days, ulong[] includeRoleIds)
{
Days = days;
IncludeRoleIds = includeRoleIds;
}
}
}

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

@@ -46,12 +46,12 @@ namespace Discord.Rest
_restLogger = LogManager.CreateLogger("Rest");
_isFirstLogin = config.DisplayInitialLog;

ApiClient.RequestQueue.RateLimitTriggered += async (id, info) =>
ApiClient.RequestQueue.RateLimitTriggered += async (id, info, endpoint) =>
{
if (info == null)
await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id?.ToString() ?? "null"}").ConfigureAwait(false);
await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {endpoint} {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}").ConfigureAwait(false);
else
await _restLogger.WarningAsync($"Rate limit triggered: {id?.ToString() ?? "null"}").ConfigureAwait(false);
await _restLogger.WarningAsync($"Rate limit triggered: {endpoint} {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}").ConfigureAwait(false);
};
ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false);
}


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

@@ -853,10 +853,11 @@ namespace Discord.API
Preconditions.NotEqual(guildId, 0, nameof(guildId));
Preconditions.NotNull(args, nameof(args));
Preconditions.AtLeast(args.Days, 1, nameof(args.Days));
string endpointRoleIds = args.IncludeRoleIds?.Length > 0 ? $"&include_roles={string.Join(",", args.IncludeRoleIds)}" : "";
options = RequestOptions.CreateOrClone(options);

var ids = new BucketIds(guildId: guildId);
return await SendAsync<GetGuildPruneCountResponse>("GET", () => $"guilds/{guildId}/prune?days={args.Days}", ids, options: options).ConfigureAwait(false);
return await SendAsync<GetGuildPruneCountResponse>("GET", () => $"guilds/{guildId}/prune?days={args.Days}{endpointRoleIds}", ids, options: options).ConfigureAwait(false);
}

//Guild Bans


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

@@ -12,7 +12,7 @@ namespace Discord.Rest
/// Represents a REST-based news channel in a guild that has the same properties as a <see cref="RestTextChannel"/>.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class RestNewsChannel : RestTextChannel
public class RestNewsChannel : RestTextChannel, INewsChannel
{
internal RestNewsChannel(BaseDiscordClient discord, IGuild guild, ulong id)
:base(discord, guild, id)


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

@@ -404,9 +404,9 @@ namespace Discord.Rest
);
}
public static async Task<int> PruneUsersAsync(IGuild guild, BaseDiscordClient client,
int days, bool simulate, RequestOptions options)
int days, bool simulate, RequestOptions options, IEnumerable<ulong> includeRoleIds)
{
var args = new GuildPruneParams(days);
var args = new GuildPruneParams(days, includeRoleIds?.ToArray());
GetGuildPruneCountResponse model;
if (simulate)
model = await client.ApiClient.GetGuildPruneCountAsync(guild.Id, args, options).ConfigureAwait(false);
@@ -479,7 +479,7 @@ namespace Discord.Rest
var emote = await client.ApiClient.GetGuildEmoteAsync(guild.Id, id, options).ConfigureAwait(false);
return emote.ToEntity();
}
public static async Task<GuildEmote> CreateEmoteAsync(IGuild guild, BaseDiscordClient client, string name, Image image, Optional<IEnumerable<IRole>> roles,
public static async Task<GuildEmote> CreateEmoteAsync(IGuild guild, BaseDiscordClient client, string name, Image image, Optional<IEnumerable<IRole>> roles,
RequestOptions options)
{
var apiargs = new CreateGuildEmoteParams
@@ -494,7 +494,7 @@ namespace Discord.Rest
return emote.ToEntity();
}
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <c>null</c>.</exception>
public static async Task<GuildEmote> ModifyEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, Action<EmoteProperties> func,
public static async Task<GuildEmote> ModifyEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, Action<EmoteProperties> func,
RequestOptions options)
{
if (func == null) throw new ArgumentNullException(paramName: nameof(func));
@@ -512,7 +512,7 @@ namespace Discord.Rest
var emote = await client.ApiClient.ModifyGuildEmoteAsync(guild.Id, id, apiargs, options).ConfigureAwait(false);
return emote.ToEntity();
}
public static Task DeleteEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options)
public static Task DeleteEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options)
=> client.ApiClient.DeleteGuildEmoteAsync(guild.Id, id, options);
}
}

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

@@ -205,7 +205,7 @@ namespace Discord.Rest
role?.Update(model);
}
}
/// <inheritdoc />
public Task LeaveAsync(RequestOptions options = null)
=> GuildHelper.LeaveAsync(this, Discord, options);
@@ -631,8 +631,8 @@ namespace Discord.Rest
/// A task that represents the asynchronous prune operation. The task result contains the number of users to
/// be or has been removed from this guild.
/// </returns>
public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null)
=> GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options);
public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null, IEnumerable<ulong> includeRoleIds = null)
=> GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options, includeRoleIds);

/// <summary>
/// Gets a collection of users in this guild that the name or nickname starts with the


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

@@ -160,10 +160,10 @@ namespace Discord.Rest
=> MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling);

/// <inheritdoc />
/// <exception cref="InvalidOperationException">This operation may only be called on a <see cref="RestNewsChannel"/> channel.</exception>
/// <exception cref="InvalidOperationException">This operation may only be called on a <see cref="INewsChannel"/> channel.</exception>
public async Task CrosspostAsync(RequestOptions options = null)
{
if (!(Channel is RestNewsChannel))
if (!(Channel is INewsChannel))
{
throw new InvalidOperationException("Publishing (crossposting) is only valid in news channels.");
}


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

@@ -21,6 +21,12 @@ namespace Discord.Rest
public string[] RPCOrigins { get; private set; }
/// <inheritdoc />
public ulong Flags { get; private set; }
/// <inheritdoc />
public bool IsBotPublic { get; private set; }
/// <inheritdoc />
public bool BotRequiresCodeGrant { get; private set; }
/// <inheritdoc />
public ITeam Team { get; private set; }

/// <inheritdoc />
public IUser Owner { get; private set; }
@@ -46,11 +52,15 @@ namespace Discord.Rest
RPCOrigins = model.RPCOrigins;
Name = model.Name;
_iconId = model.Icon;
IsBotPublic = model.IsBotPublic;
BotRequiresCodeGrant = model.BotRequiresCodeGrant;

if (model.Flags.IsSpecified)
Flags = model.Flags.Value; //TODO: Do we still need this?
if (model.Owner.IsSpecified)
Owner = RestUser.Create(Discord, model.Owner.Value);
if (model.Team != null)
Team = RestTeam.Create(Discord, model.Team);
}

/// <exception cref="InvalidOperationException">Unable to update this object from a different application token.</exception>


+ 37
- 0
src/Discord.Net.Rest/Entities/Teams/RestTeam.cs View File

@@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Model = Discord.API.Team;

namespace Discord.Rest
{
public class RestTeam : RestEntity<ulong>, ITeam
{
/// <inheritdoc />
public string IconUrl => _iconId != null ? CDN.GetTeamIconUrl(Id, _iconId) : null;
/// <inheritdoc />
public IReadOnlyList<ITeamMember> TeamMembers { get; private set; }
/// <inheritdoc />
public ulong OwnerUserId { get; private set; }

private string _iconId;

internal RestTeam(BaseDiscordClient discord, ulong id)
: base(discord, id)
{
}
internal static RestTeam Create(BaseDiscordClient discord, Model model)
{
var entity = new RestTeam(discord, model.Id);
entity.Update(model);
return entity;
}
internal virtual void Update(Model model)
{
if (model.Icon.IsSpecified)
_iconId = model.Icon.Value;
OwnerUserId = model.OwnerUserId;
TeamMembers = model.TeamMembers.Select(x => new RestTeamMember(Discord, x)).ToImmutableArray();
}
}
}

+ 30
- 0
src/Discord.Net.Rest/Entities/Teams/RestTeamMember.cs View File

@@ -0,0 +1,30 @@
using System;
using Model = Discord.API.TeamMember;

namespace Discord.Rest
{
public class RestTeamMember : ITeamMember
{
/// <inheritdoc />
public MembershipState MembershipState { get; }
/// <inheritdoc />
public string[] Permissions { get; }
/// <inheritdoc />
public ulong TeamId { get; }
/// <inheritdoc />
public IUser User { get; }

internal RestTeamMember(BaseDiscordClient discord, Model model)
{
MembershipState = model.MembershipState switch
{
API.MembershipState.Invited => MembershipState.Invited,
API.MembershipState.Accepted => MembershipState.Accepted,
_ => throw new InvalidOperationException("Invalid membership state"),
};
Permissions = model.Permissions;
TeamId = model.TeamId;
User = RestUser.Create(discord, model.User);
}
}
}

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

@@ -12,7 +12,7 @@ namespace Discord.Net.Queue
{
internal class RequestQueue : IDisposable
{
public event Func<BucketId, RateLimitInfo?, Task> RateLimitTriggered;
public event Func<BucketId, RateLimitInfo?, string, Task> RateLimitTriggered;

private readonly ConcurrentDictionary<BucketId, object> _buckets;
private readonly SemaphoreSlim _tokenLock;
@@ -121,9 +121,9 @@ namespace Discord.Net.Queue
}
return (RequestBucket)obj;
}
internal async Task RaiseRateLimitTriggered(BucketId bucketId, RateLimitInfo? info)
internal async Task RaiseRateLimitTriggered(BucketId bucketId, RateLimitInfo? info, string endpoint)
{
await RateLimitTriggered(bucketId, info).ConfigureAwait(false);
await RateLimitTriggered(bucketId, info, endpoint).ConfigureAwait(false);
}
internal (RequestBucket, BucketId) UpdateBucketHash(BucketId id, string discordHash)
{


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

@@ -84,7 +84,7 @@ namespace Discord.Net.Queue
#endif
UpdateRateLimit(id, request, info, true);
}
await _queue.RaiseRateLimitTriggered(Id, info).ConfigureAwait(false);
await _queue.RaiseRateLimitTriggered(Id, info, $"{request.Method} {request.Endpoint}").ConfigureAwait(false);
continue; //Retry
case HttpStatusCode.BadGateway: //502
#if DEBUG_LIMITS
@@ -187,7 +187,7 @@ namespace Discord.Net.Queue
if (!isRateLimited)
{
isRateLimited = true;
await _queue.RaiseRateLimitTriggered(Id, null).ConfigureAwait(false);
await _queue.RaiseRateLimitTriggered(Id, null, $"{request.Method} {request.Endpoint}").ConfigureAwait(false);
}

ThrowRetryLimit(request);


+ 31
- 0
src/Discord.Net.WebSocket/API/Gateway/InviteCreateEvent.cs View File

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

namespace Discord.API.Gateway
{
internal class InviteCreateEvent
{
[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }
[JsonProperty("code")]
public string Code { get; set; }
[JsonProperty("created_at")]
public DateTimeOffset CreatedAt { get; set; }
[JsonProperty("guild_id")]
public Optional<ulong> GuildId { get; set; }
[JsonProperty("inviter")]
public Optional<User> Inviter { get; set; }
[JsonProperty("max_age")]
public int MaxAge { get; set; }
[JsonProperty("max_uses")]
public int MaxUses { get; set; }
[JsonProperty("target_user")]
public Optional<User> TargetUser { get; set; }
[JsonProperty("target_user_type")]
public Optional<TargetUserType> TargetUserType { get; set; }
[JsonProperty("temporary")]
public bool Temporary { get; set; }
[JsonProperty("uses")]
public int Uses { get; set; }
}
}

+ 14
- 0
src/Discord.Net.WebSocket/API/Gateway/InviteDeleteEvent.cs View File

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

namespace Discord.API.Gateway
{
internal class InviteDeleteEvent
{
[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }
[JsonProperty("code")]
public string Code { get; set; }
[JsonProperty("guild_id")]
public Optional<ulong> GuildId { get; set; }
}
}

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

@@ -99,6 +99,12 @@ namespace Discord.Audio
_token = token;
await _connection.StartAsync().ConfigureAwait(false);
}

public IReadOnlyDictionary<ulong, AudioInStream> GetStreams()
{
return _streams.ToDictionary(pair => pair.Key, pair => pair.Value.Reader);
}

public async Task StopAsync()
{
await _connection.StopAsync().ConfigureAwait(false);
@@ -379,7 +385,7 @@ namespace Discord.Audio

private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken)
{
//TODO: Clean this up when Discord's session patch is live
// TODO: Clean this up when Discord's session patch is live
try
{
await _audioLogger.DebugAsync("Heartbeat Started").ConfigureAwait(false);


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

@@ -61,14 +61,17 @@ namespace Discord.Audio.Streams

_task = Run();
}

protected override void Dispose(bool disposing)
{
if (disposing)
{
_disposeTokenSource?.Cancel();
_disposeTokenSource?.Dispose();
_cancelTokenSource?.Cancel();
_cancelTokenSource?.Dispose();
_queueLock?.Dispose();
_next.Dispose();
}
base.Dispose(disposing);
}


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

@@ -68,10 +68,12 @@ namespace Discord.Audio.Streams

protected override void Dispose(bool disposing)
{
base.Dispose(disposing);

if (disposing)
{
_decoder.Dispose();
_next.Dispose();
}
base.Dispose(disposing);
}
}
}

+ 5
- 3
src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs View File

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

@@ -89,10 +89,12 @@ namespace Discord.Audio.Streams

protected override void Dispose(bool disposing)
{
base.Dispose(disposing);

if (disposing)
{
_encoder.Dispose();
_next.Dispose();
}
base.Dispose(disposing);
}
}
}

+ 7
- 0
src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs View File

@@ -76,5 +76,12 @@ namespace Discord.Audio.Streams
(buffer[extensionOffset + 3]);
return extensionOffset + 4 + (extensionLength * 4);
}

protected override void Dispose(bool disposing)
{
if (disposing)
_next.Dispose();
base.Dispose(disposing);
}
}
}

+ 8
- 1
src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs View File

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

@@ -69,5 +69,12 @@ namespace Discord.Audio.Streams
{
await _next.ClearAsync(cancelToken).ConfigureAwait(false);
}

protected override void Dispose(bool disposing)
{
if (disposing)
_next.Dispose();
base.Dispose(disposing);
}
}
}

+ 7
- 0
src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs View File

@@ -44,5 +44,12 @@ namespace Discord.Audio.Streams
{
await _next.ClearAsync(cancelToken).ConfigureAwait(false);
}

protected override void Dispose(bool disposing)
{
if (disposing)
_next.Dispose();
base.Dispose(disposing);
}
}
}

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

@@ -60,5 +60,12 @@ namespace Discord.Audio.Streams
{
await _next.ClearAsync(cancelToken).ConfigureAwait(false);
}

protected override void Dispose(bool disposing)
{
if (disposing)
_next.Dispose();
base.Dispose(disposing);
}
}
}

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

@@ -389,5 +389,47 @@ namespace Discord.WebSocket
remove { _recipientRemovedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGroupUser, Task>> _recipientRemovedEvent = new AsyncEvent<Func<SocketGroupUser, Task>>();

//Invites
/// <summary>
/// Fired when an invite is created.
/// </summary>
/// <remarks>
/// <para>
/// This event is fired when an invite is created. The event handler must return a
/// <see cref="Task"/> and accept a <see cref="SocketInvite"/> as its parameter.
/// </para>
/// <para>
/// The invite created will be passed into the <see cref="SocketInvite"/> parameter.
/// </para>
/// </remarks>
public event Func<SocketInvite, Task> InviteCreated
{
add { _inviteCreatedEvent.Add(value); }
remove { _inviteCreatedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketInvite, Task>> _inviteCreatedEvent = new AsyncEvent<Func<SocketInvite, Task>>();
/// <summary>
/// Fired when an invite is deleted.
/// </summary>
/// <remarks>
/// <para>
/// This event is fired when an invite is deleted. The event handler must return
/// a <see cref="Task"/> and accept a <see cref="SocketGuildChannel"/> and
/// <see cref="string"/> as its parameter.
/// </para>
/// <para>
/// The channel where this invite was created will be passed into the <see cref="SocketGuildChannel"/> parameter.
/// </para>
/// <para>
/// The code of the deleted invite will be passed into the <see cref="string"/> parameter.
/// </para>
/// </remarks>
public event Func<SocketGuildChannel, string, Task> InviteDeleted
{
add { _inviteDeletedEvent.Add(value); }
remove { _inviteDeletedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuildChannel, string, Task>> _inviteDeletedEvent = new AsyncEvent<Func<SocketGuildChannel, string, Task>>();
}
}

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

@@ -338,6 +338,9 @@ namespace Discord.WebSocket
client.UserIsTyping += (oldUser, newUser) => _userIsTypingEvent.InvokeAsync(oldUser, newUser);
client.RecipientAdded += (user) => _recipientAddedEvent.InvokeAsync(user);
client.RecipientRemoved += (user) => _recipientRemovedEvent.InvokeAsync(user);

client.InviteCreated += (invite) => _inviteCreatedEvent.InvokeAsync(invite);
client.InviteDeleted += (channel, invite) => _inviteDeletedEvent.InvokeAsync(channel, invite);
}

//IDiscordClient


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

@@ -370,7 +370,7 @@ namespace Discord.WebSocket
{
var cachedGuilds = guilds.ToImmutableArray();

int batchSize = _gatewayIntents.HasValue ? 1 : 100;
const short batchSize = 1;
ulong[] batchIds = new ulong[Math.Min(batchSize, cachedGuilds.Length)];
Task[] batchTasks = new Task[batchIds.Length];
int batchCount = (cachedGuilds.Length + (batchSize - 1)) / batchSize;
@@ -1688,6 +1688,64 @@ namespace Discord.WebSocket
}
break;

//Invites
case "INVITE_CREATE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_CREATE)").ConfigureAwait(false);

var data = (payload as JToken).ToObject<API.Gateway.InviteCreateEvent>(_serializer);
if (State.GetChannel(data.ChannelId) is SocketGuildChannel channel)
{
var guild = channel.Guild;
if (!guild.IsSynced)
{
await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false);
return;
}

SocketGuildUser inviter = data.Inviter.IsSpecified
? (guild.GetUser(data.Inviter.Value.Id) ?? guild.AddOrUpdateUser(data.Inviter.Value))
: null;

SocketUser target = data.TargetUser.IsSpecified
? (guild.GetUser(data.TargetUser.Value.Id) ?? (SocketUser)SocketUnknownUser.Create(this, State, data.TargetUser.Value))
: null;

var invite = SocketInvite.Create(this, guild, channel, inviter, target, data);

await TimedInvokeAsync(_inviteCreatedEvent, nameof(InviteCreated), invite).ConfigureAwait(false);
}
else
{
await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false);
return;
}
}
break;
case "INVITE_DELETE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_DELETE)").ConfigureAwait(false);

var data = (payload as JToken).ToObject<API.Gateway.InviteDeleteEvent>(_serializer);
if (State.GetChannel(data.ChannelId) is SocketGuildChannel channel)
{
var guild = channel.Guild;
if (!guild.IsSynced)
{
await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false);
return;
}

await TimedInvokeAsync(_inviteDeletedEvent, nameof(InviteDeleted), channel, data.Code).ConfigureAwait(false);
}
else
{
await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false);
return;
}
}
break;

//Ignored (User only)
case "CHANNEL_PINS_ACK":
await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_ACK)").ConfigureAwait(false);


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

@@ -150,7 +150,8 @@ namespace Discord.WebSocket
}
}
private int _maxWaitForGuildAvailable = 10000;

/// <summary>
/// Gets or sets gateway intents to limit what events are sent from Discord. Allows for more granular control than the <see cref="GuildSubscriptions"/> property.
/// </summary>
/// <remarks>


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

@@ -15,7 +15,7 @@ namespace Discord.WebSocket
/// </note>
/// </remarks>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketNewsChannel : SocketTextChannel
public class SocketNewsChannel : SocketTextChannel, INewsChannel
{
internal SocketNewsChannel(DiscordSocketClient discord, ulong id, SocketGuild guild)
:base(discord, id, guild)


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

@@ -746,8 +746,8 @@ namespace Discord.WebSocket
return null;
}
/// <inheritdoc />
public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null)
=> GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options);
public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null, IEnumerable<ulong> includeRoleIds = null)
=> GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options, includeRoleIds);

internal SocketGuildUser AddOrUpdateUser(UserModel model)
{


+ 143
- 0
src/Discord.Net.WebSocket/Entities/Invites/SocketInvite.cs View File

@@ -0,0 +1,143 @@
using Discord.Rest;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Model = Discord.API.Gateway.InviteCreateEvent;

namespace Discord.WebSocket
{
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketInvite : SocketEntity<string>, IInviteMetadata
{
private long _createdAtTicks;

/// <inheritdoc />
public ulong ChannelId { get; private set; }
/// <summary>
/// Gets the channel where this invite was created.
/// </summary>
public SocketGuildChannel Channel { get; private set; }
/// <inheritdoc />
public ulong? GuildId { get; private set; }
/// <summary>
/// Gets the guild where this invite was created.
/// </summary>
public SocketGuild Guild { get; private set; }
/// <inheritdoc />
ChannelType IInvite.ChannelType
{
get
{
switch (Channel)
{
case IVoiceChannel voiceChannel: return ChannelType.Voice;
case ICategoryChannel categoryChannel: return ChannelType.Category;
case IDMChannel dmChannel: return ChannelType.DM;
case IGroupChannel groupChannel: return ChannelType.Group;
case INewsChannel newsChannel: return ChannelType.News;
case ITextChannel textChannel: return ChannelType.Text;
default: throw new InvalidOperationException("Invalid channel type.");
}
}
}
/// <inheritdoc />
string IInvite.ChannelName => Channel.Name;
/// <inheritdoc />
string IInvite.GuildName => Guild.Name;
/// <inheritdoc />
int? IInvite.PresenceCount => throw new NotImplementedException();
/// <inheritdoc />
int? IInvite.MemberCount => throw new NotImplementedException();
/// <inheritdoc />
bool IInviteMetadata.IsRevoked => throw new NotImplementedException();
/// <inheritdoc />
public bool IsTemporary { get; private set; }
/// <inheritdoc />
int? IInviteMetadata.MaxAge { get => MaxAge; }
/// <inheritdoc />
int? IInviteMetadata.MaxUses { get => MaxUses; }
/// <inheritdoc />
int? IInviteMetadata.Uses { get => Uses; }
/// <summary>
/// Gets the time (in seconds) until the invite expires.
/// </summary>
public int MaxAge { get; private set; }
/// <summary>
/// Gets the max number of uses this invite may have.
/// </summary>
public int MaxUses { get; private set; }
/// <summary>
/// Gets the number of times this invite has been used.
/// </summary>
public int Uses { get; private set; }
/// <summary>
/// Gets the user that created this invite if available.
/// </summary>
public SocketGuildUser Inviter { get; private set; }
/// <inheritdoc />
DateTimeOffset? IInviteMetadata.CreatedAt => DateTimeUtils.FromTicks(_createdAtTicks);
/// <summary>
/// Gets when this invite was created.
/// </summary>
public DateTimeOffset CreatedAt => DateTimeUtils.FromTicks(_createdAtTicks);
/// <summary>
/// Gets the user targeted by this invite if available.
/// </summary>
public SocketUser TargetUser { get; private set; }
/// <summary>
/// Gets the type of the user targeted by this invite.
/// </summary>
public TargetUserType TargetUserType { get; private set; }

/// <inheritdoc />
public string Code => Id;
/// <inheritdoc />
public string Url => $"{DiscordConfig.InviteUrl}{Code}";

internal SocketInvite(DiscordSocketClient discord, SocketGuild guild, SocketGuildChannel channel, SocketGuildUser inviter, SocketUser target, string id)
: base(discord, id)
{
Guild = guild;
Channel = channel;
Inviter = inviter;
TargetUser = target;
}
internal static SocketInvite Create(DiscordSocketClient discord, SocketGuild guild, SocketGuildChannel channel, SocketGuildUser inviter, SocketUser target, Model model)
{
var entity = new SocketInvite(discord, guild, channel, inviter, target, model.Code);
entity.Update(model);
return entity;
}
internal void Update(Model model)
{
ChannelId = model.ChannelId;
GuildId = model.GuildId.IsSpecified ? model.GuildId.Value : Guild.Id;
IsTemporary = model.Temporary;
MaxAge = model.MaxAge;
MaxUses = model.MaxUses;
Uses = model.Uses;
_createdAtTicks = model.CreatedAt.UtcTicks;
TargetUserType = model.TargetUserType.IsSpecified ? model.TargetUserType.Value : TargetUserType.Undefined;
}

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

/// <summary>
/// Gets the URL of the invite.
/// </summary>
/// <returns>
/// A string that resolves to the Url of the invite.
/// </returns>
public override string ToString() => Url;
private string DebuggerDisplay => $"{Url} ({Guild?.Name} / {Channel.Name})";

/// <inheritdoc />
IGuild IInvite.Guild => Guild;
/// <inheritdoc />
IChannel IInvite.Channel => Channel;
/// <inheritdoc />
IUser IInviteMetadata.Inviter => Inviter;
}
}

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

@@ -164,10 +164,10 @@ namespace Discord.WebSocket
=> MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling);

/// <inheritdoc />
/// <exception cref="InvalidOperationException">This operation may only be called on a <see cref="SocketNewsChannel"/> channel.</exception>
/// <exception cref="InvalidOperationException">This operation may only be called on a <see cref="INewsChannel"/> channel.</exception>
public async Task CrosspostAsync(RequestOptions options = null)
{
if (!(Channel is SocketNewsChannel))
if (!(Channel is INewsChannel))
{
throw new InvalidOperationException("Publishing (crossposting) is only valid in news channels.");
}


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

@@ -74,12 +74,12 @@ namespace Discord.Webhook

_restLogger = LogManager.CreateLogger("Rest");

ApiClient.RequestQueue.RateLimitTriggered += async (id, info) =>
ApiClient.RequestQueue.RateLimitTriggered += async (id, info, endpoint) =>
{
if (info == null)
await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id?.ToString() ?? "null"}").ConfigureAwait(false);
await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {endpoint} {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}").ConfigureAwait(false);
else
await _restLogger.WarningAsync($"Rate limit triggered: {id?.ToString() ?? "null"}").ConfigureAwait(false);
await _restLogger.WarningAsync($"Rate limit triggered: {endpoint} {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}").ConfigureAwait(false);
};
ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false);
}


+ 2
- 0
test/Discord.Net.Tests.Unit/GuildPermissionsTests.cs View File

@@ -69,6 +69,7 @@ namespace Discord
AssertFlag(() => new GuildPermissions(manageGuild: true), GuildPermission.ManageGuild);
AssertFlag(() => new GuildPermissions(addReactions: true), GuildPermission.AddReactions);
AssertFlag(() => new GuildPermissions(viewAuditLog: true), GuildPermission.ViewAuditLog);
AssertFlag(() => new GuildPermissions(viewGuildInsights: true), GuildPermission.ViewGuildInsights);
AssertFlag(() => new GuildPermissions(viewChannel: true), GuildPermission.ViewChannel);
AssertFlag(() => new GuildPermissions(sendMessages: true), GuildPermission.SendMessages);
AssertFlag(() => new GuildPermissions(sendTTSMessages: true), GuildPermission.SendTTSMessages);
@@ -141,6 +142,7 @@ namespace Discord
AssertUtil(GuildPermission.ManageGuild, x => x.ManageGuild, (p, enable) => p.Modify(manageGuild: enable));
AssertUtil(GuildPermission.AddReactions, x => x.AddReactions, (p, enable) => p.Modify(addReactions: enable));
AssertUtil(GuildPermission.ViewAuditLog, x => x.ViewAuditLog, (p, enable) => p.Modify(viewAuditLog: enable));
AssertUtil(GuildPermission.ViewGuildInsights, x => x.ViewGuildInsights, (p, enable) => p.Modify(viewGuildInsights: enable));
AssertUtil(GuildPermission.ViewChannel, x => x.ViewChannel, (p, enable) => p.Modify(viewChannel: enable));
AssertUtil(GuildPermission.SendMessages, x => x.SendMessages, (p, enable) => p.Modify(sendMessages: enable));
AssertUtil(GuildPermission.SendTTSMessages, x => x.SendTTSMessages, (p, enable) => p.Modify(sendTTSMessages: enable));


Loading…
Cancel
Save