From bceeab2d0c05fa6fd5072acac92b8e1bf66571e4 Mon Sep 17 00:00:00 2001 From: quin lynch Date: Mon, 1 Nov 2021 16:45:17 -0300 Subject: [PATCH] Guild feature revamp and smart gateway intent checks --- .../Entities/Guilds/GuildFeature.cs | 105 ++++++++++++++++++ .../Entities/Guilds/GuildFeatures.cs | 46 ++++++++ .../Entities/Guilds/IGuild.cs | 6 +- src/Discord.Net.Rest/API/Common/Guild.cs | 2 +- .../Entities/Channels/ThreadHelper.cs | 7 +- .../Entities/Guilds/RestGuild.cs | 11 +- .../Entities/Roles/RoleHelper.cs | 6 + .../Net/Converters/DiscordContractResolver.cs | 2 + .../Net/Converters/GuildFeaturesConverter.cs | 60 ++++++++++ .../DiscordSocketClient.cs | 14 +++ .../Entities/Guilds/SocketGuild.cs | 11 +- 11 files changed, 247 insertions(+), 23 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Guilds/GuildFeature.cs create mode 100644 src/Discord.Net.Core/Entities/Guilds/GuildFeatures.cs create mode 100644 src/Discord.Net.Rest/Net/Converters/GuildFeaturesConverter.cs diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildFeature.cs b/src/Discord.Net.Core/Entities/Guilds/GuildFeature.cs new file mode 100644 index 000000000..e3c325227 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/GuildFeature.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + [Flags] + public enum GuildFeature + { + /// + /// The guild has no features. + /// + None = 0, + /// + /// The guild has access to set an animated guild icon. + /// + AnimatedIcon = 1 << 0, + /// + /// The guild has access to set a guild banner image. + /// + Banner = 1 << 1, + /// + /// The guild has access to use commerce features (i.e. create store channels). + /// + Commerce = 1 << 2, + /// + /// The guild can enable welcome screen, Membership Screening, stage channels and discovery, and receives community updates. + /// + Community = 1 << 3, + /// + /// The guild is able to be discovered in the directory. + /// + Discoverable = 1 << 4, + /// + /// The guild is able to be featured in the directory. + /// + Featureable = 1 << 5, + /// + /// The guild has access to set an invite splash background. + /// + InviteSplash = 1 << 6, + /// + /// The guild has enabled Membership Screening. + /// + MemberVerificationGateEnabled = 1 << 7, + /// + /// The guild has enabled monetization. + /// + MonetizationEnabled = 1 << 8, + /// + /// The guild has increased custom sticker slots. + /// + MoreStickers = 1 << 9, + /// + /// The guild has access to create news channels. + /// + News = 1 << 10, + /// + /// The guild is partnered. + /// + Partnered = 1 << 11, + /// + /// The guild can be previewed before joining via Membership Screening or the directory. + /// + PreviewEnabled = 1 << 12, + /// + /// The guild has access to create private threads. + /// + PrivateThreads = 1 << 13, + /// + /// The guild is able to set role icons. + /// + RoleIcons = 1 << 14, + /// + /// The guild has access to the seven day archive time for threads. + /// + SevenDayThreadArchive = 1 << 15, + /// + /// The guild has access to the three day archive time for threads. + /// + ThreeDayThreadArchive = 1 << 16, + /// + /// The guild has enabled ticketed events. + /// + TicketedEventsEnabled = 1 << 17, + /// + /// The guild has access to set a vanity URL. + /// + VanityUrl = 1 << 18, + /// + /// The guild is verified. + /// + Verified = 1 << 19, + /// + /// The guild has access to set 384kbps bitrate in voice (previously VIP voice servers). + /// + VIPRegions = 1 << 20, + /// + /// The guild has enabled the welcome screen. + /// + WelcomeScreenEnabled = 1 << 21, + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildFeatures.cs b/src/Discord.Net.Core/Entities/Guilds/GuildFeatures.cs new file mode 100644 index 000000000..699e47cf3 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/GuildFeatures.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public class GuildFeatures + { + /// + /// Gets the flags of recognized features for this guild. + /// + public GuildFeature Value { get; } + + /// + /// Gets a collection of experimental features for this guild. + /// + public IReadOnlyCollection Experimental { get; } + + + internal GuildFeatures(GuildFeature value, string[] experimental) + { + Value = value; + Experimental = experimental.ToImmutableArray(); + } + + public bool HasFeature(GuildFeature feature) + => Value.HasFlag(feature); + public bool HasFeature(string feature) + => Experimental.Contains(feature); + + internal void EnsureFeature(GuildFeature feature) + { + if (!HasFeature(feature)) + { + var vals = Enum.GetValues(typeof(GuildFeature)).Cast(); + + var missingValues = vals.Where(x => feature.HasFlag(x) && !Value.HasFlag(x)); + + throw new InvalidOperationException($"Missing required guild feature{(missingValues.Count() > 1 ? "s" : "")} {string.Join(", ", missingValues.Select(x => x.ToString()))} in order to execute this operation."); + } + } + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index fe8fe43f0..fa76cc360 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -207,12 +207,12 @@ namespace Discord /// IReadOnlyCollection Stickers { get; } /// - /// Gets a collection of all extra features added to this guild. + /// Gets the features for this guild. /// /// - /// A read-only collection of enabled features in this guild. + /// A flags enum containing all the features for the guild. /// - IReadOnlyCollection Features { get; } + GuildFeatures Features { get; } /// /// Gets a collection of all roles in this guild. /// diff --git a/src/Discord.Net.Rest/API/Common/Guild.cs b/src/Discord.Net.Rest/API/Common/Guild.cs index 39516f188..d550c54a0 100644 --- a/src/Discord.Net.Rest/API/Common/Guild.cs +++ b/src/Discord.Net.Rest/API/Common/Guild.cs @@ -35,7 +35,7 @@ namespace Discord.API [JsonProperty("emojis")] public Emoji[] Emojis { get; set; } [JsonProperty("features")] - public string[] Features { get; set; } + public GuildFeatures Features { get; set; } [JsonProperty("mfa_level")] public MfaLevel MfaLevel { get; set; } [JsonProperty("application_id")] diff --git a/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs index 69eb0d768..4c2bec8e2 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs @@ -11,13 +11,14 @@ namespace Discord.Rest public static async Task CreateThreadAsync(BaseDiscordClient client, ITextChannel channel, string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, RequestOptions options = null) { - if (autoArchiveDuration == ThreadArchiveDuration.OneWeek && !channel.Guild.Features.Contains("SEVEN_DAY_THREAD_ARCHIVE")) + var features = channel.Guild.Features; + if (autoArchiveDuration == ThreadArchiveDuration.OneWeek && !features.HasFeature(GuildFeature.SevenDayThreadArchive)) throw new ArgumentException($"The guild {channel.Guild.Name} does not have the SEVEN_DAY_THREAD_ARCHIVE feature!", nameof(autoArchiveDuration)); - if (autoArchiveDuration == ThreadArchiveDuration.ThreeDays && !channel.Guild.Features.Contains("THREE_DAY_THREAD_ARCHIVE")) + if (autoArchiveDuration == ThreadArchiveDuration.ThreeDays && !features.HasFeature(GuildFeature.ThreeDayThreadArchive)) throw new ArgumentException($"The guild {channel.Guild.Name} does not have the THREE_DAY_THREAD_ARCHIVE feature!", nameof(autoArchiveDuration)); - if (type == ThreadType.PrivateThread && !channel.Guild.Features.Contains("PRIVATE_THREADS")) + if (type == ThreadType.PrivateThread && !features.HasFeature(GuildFeature.PrivateThreads)) throw new ArgumentException($"The guild {channel.Guild.Name} does not have the PRIVATE_THREADS feature!", nameof(type)); var args = new StartThreadParams diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 476764715..26dc8f3b7 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -22,7 +22,6 @@ namespace Discord.Rest private ImmutableDictionary _roles; private ImmutableArray _emotes; private ImmutableArray _stickers; - private ImmutableArray _features; /// public string Name { get; private set; } @@ -90,9 +89,10 @@ namespace Discord.Rest public NsfwLevel NsfwLevel { get; private set; } /// public bool IsBoostProgressBarEnabled { get; private set; } - /// public CultureInfo PreferredCulture { get; private set; } + /// + public GuildFeatures Features { get; private set; } /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); @@ -118,8 +118,6 @@ namespace Discord.Rest /// public IReadOnlyCollection Emotes => _emotes; public IReadOnlyCollection Stickers => _stickers; - /// - public IReadOnlyCollection Features => _features; internal RestGuild(BaseDiscordClient client, ulong id) : base(client, id) @@ -185,10 +183,7 @@ namespace Discord.Rest else _emotes = ImmutableArray.Create(); - if (model.Features != null) - _features = model.Features.ToImmutableArray(); - else - _features = ImmutableArray.Create(); + Features = model.Features; var roles = ImmutableDictionary.CreateBuilder(); if (model.Roles != null) diff --git a/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs b/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs index 1211ec40b..d8552f869 100644 --- a/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs +++ b/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs @@ -18,6 +18,12 @@ namespace Discord.Rest { var args = new RoleProperties(); func(args); + + if (args.Icon.IsSpecified) + { + role.Guild.Features.EnsureFeature(GuildFeature.RoleIcons); + } + var apiArgs = new API.Rest.ModifyGuildRoleParams { Color = args.Color.IsSpecified ? args.Color.Value.RawValue : Optional.Create(), diff --git a/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs index feea164f9..6fe44bf4e 100644 --- a/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs @@ -87,6 +87,8 @@ namespace Discord.Net.Converters return MessageComponentConverter.Instance; if (type == typeof(API.Interaction)) return InteractionConverter.Instance; + if (type == typeof(GuildFeatures)) + return GuildFeaturesConverter.Instance; //Entities var typeInfo = type.GetTypeInfo(); diff --git a/src/Discord.Net.Rest/Net/Converters/GuildFeaturesConverter.cs b/src/Discord.Net.Rest/Net/Converters/GuildFeaturesConverter.cs new file mode 100644 index 000000000..9f82b440b --- /dev/null +++ b/src/Discord.Net.Rest/Net/Converters/GuildFeaturesConverter.cs @@ -0,0 +1,60 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Discord.Net.Converters +{ + internal class GuildFeaturesConverter : JsonConverter + { + public static GuildFeaturesConverter Instance + => new GuildFeaturesConverter(); + + public override bool CanConvert(Type objectType) => true; + public override bool CanWrite => false; + public override bool CanRead => true; + + + private Regex _readRegex = new Regex(@"_(\w)"); + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var obj = JToken.Load(reader); + var arr = obj.ToObject(); + + GuildFeature features = GuildFeature.None; + List experimental = new(); + + foreach(var item in arr) + { + var name = _readRegex.Replace(item.ToLower(), (x) => + { + return x.Groups[1].Value.ToUpper(); + }); + + name = name[0].ToString().ToUpper() + new string(name.Skip(1).ToArray()); + + try + { + var result = (GuildFeature)Enum.Parse(typeof(GuildFeature), name); + + features |= result; + } + catch + { + experimental.Add(item); + } + } + + return new GuildFeatures(features, experimental.ToArray()); + } + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 07303873d..66a614980 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -572,6 +572,8 @@ namespace Discord.WebSocket { if (ConnectionState == ConnectionState.Connected) { + EnsureGatewayIntent(GatewayIntents.GuildMembers); + //Race condition leads to guilds being requested twice, probably okay await ProcessUserDownloadsAsync(guilds.Select(x => GetGuild(x.Id)).Where(x => x != null)).ConfigureAwait(false); } @@ -2717,6 +2719,18 @@ namespace Discord.WebSocket channel.Recipient.GlobalUser.RemoveRef(this); } + internal void EnsureGatewayIntent(GatewayIntents intents) + { + if (!_gatewayIntents.HasFlag(intents)) + { + var vals = Enum.GetValues(typeof(GatewayIntents)).Cast(); + + var missingValues = vals.Where(x => intents.HasFlag(x) && !_gatewayIntents.HasFlag(x)); + + throw new InvalidOperationException($"Missing required gateway intent{(missingValues.Count() > 1 ? "s" : "")} {string.Join(", ", missingValues.Select(x => x.ToString()))} in order to execute this operation."); + } + } + private async Task GuildAvailableAsync(SocketGuild guild) { if (!guild.IsConnected) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 0ab439ffd..e190f9b23 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -42,7 +42,6 @@ namespace Discord.WebSocket private ConcurrentDictionary _stickers; private ImmutableArray _emotes; - private ImmutableArray _features; private AudioClient _audioClient; #pragma warning restore IDISP002, IDISP006 @@ -129,6 +128,8 @@ namespace Discord.WebSocket public CultureInfo PreferredCulture { get; private set; } /// public bool IsBoostProgressBarEnabled { get; private set; } + /// + public GuildFeatures Features { get; private set; } /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); @@ -333,8 +334,6 @@ namespace Discord.WebSocket /// public IReadOnlyCollection Stickers => _stickers.Select(x => x.Value).ToImmutableArray(); - /// - public IReadOnlyCollection Features => _features; /// /// Gets a collection of users in this guild. /// @@ -370,7 +369,6 @@ namespace Discord.WebSocket { _audioLock = new SemaphoreSlim(1, 1); _emotes = ImmutableArray.Create(); - _features = ImmutableArray.Create(); } internal static SocketGuild Create(DiscordSocketClient discord, ClientState state, ExtendedModel model) { @@ -508,10 +506,7 @@ namespace Discord.WebSocket else _emotes = ImmutableArray.Create(); - if (model.Features != null) - _features = model.Features.ToImmutableArray(); - else - _features = ImmutableArray.Create(); + Features = model.Features; var roles = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Roles.Length * 1.05)); if (model.Roles != null)