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)