@@ -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 | |||||
{ | |||||
/// <summary> | |||||
/// The guild has no features. | |||||
/// </summary> | |||||
None = 0, | |||||
/// <summary> | |||||
/// The guild has access to set an animated guild icon. | |||||
/// </summary> | |||||
AnimatedIcon = 1 << 0, | |||||
/// <summary> | |||||
/// The guild has access to set a guild banner image. | |||||
/// </summary> | |||||
Banner = 1 << 1, | |||||
/// <summary> | |||||
/// The guild has access to use commerce features (i.e. create store channels). | |||||
/// </summary> | |||||
Commerce = 1 << 2, | |||||
/// <summary> | |||||
/// The guild can enable welcome screen, Membership Screening, stage channels and discovery, and receives community updates. | |||||
/// </summary> | |||||
Community = 1 << 3, | |||||
/// <summary> | |||||
/// The guild is able to be discovered in the directory. | |||||
/// </summary> | |||||
Discoverable = 1 << 4, | |||||
/// <summary> | |||||
/// The guild is able to be featured in the directory. | |||||
/// </summary> | |||||
Featureable = 1 << 5, | |||||
/// <summary> | |||||
/// The guild has access to set an invite splash background. | |||||
/// </summary> | |||||
InviteSplash = 1 << 6, | |||||
/// <summary> | |||||
/// The guild has enabled <seealso href="https://discord.com/developers/docs/resources/guild#membership-screening-object">Membership Screening</seealso>. | |||||
/// </summary> | |||||
MemberVerificationGateEnabled = 1 << 7, | |||||
/// <summary> | |||||
/// The guild has enabled monetization. | |||||
/// </summary> | |||||
MonetizationEnabled = 1 << 8, | |||||
/// <summary> | |||||
/// The guild has increased custom sticker slots. | |||||
/// </summary> | |||||
MoreStickers = 1 << 9, | |||||
/// <summary> | |||||
/// The guild has access to create news channels. | |||||
/// </summary> | |||||
News = 1 << 10, | |||||
/// <summary> | |||||
/// The guild is partnered. | |||||
/// </summary> | |||||
Partnered = 1 << 11, | |||||
/// <summary> | |||||
/// The guild can be previewed before joining via Membership Screening or the directory. | |||||
/// </summary> | |||||
PreviewEnabled = 1 << 12, | |||||
/// <summary> | |||||
/// The guild has access to create private threads. | |||||
/// </summary> | |||||
PrivateThreads = 1 << 13, | |||||
/// <summary> | |||||
/// The guild is able to set role icons. | |||||
/// </summary> | |||||
RoleIcons = 1 << 14, | |||||
/// <summary> | |||||
/// The guild has access to the seven day archive time for threads. | |||||
/// </summary> | |||||
SevenDayThreadArchive = 1 << 15, | |||||
/// <summary> | |||||
/// The guild has access to the three day archive time for threads. | |||||
/// </summary> | |||||
ThreeDayThreadArchive = 1 << 16, | |||||
/// <summary> | |||||
/// The guild has enabled ticketed events. | |||||
/// </summary> | |||||
TicketedEventsEnabled = 1 << 17, | |||||
/// <summary> | |||||
/// The guild has access to set a vanity URL. | |||||
/// </summary> | |||||
VanityUrl = 1 << 18, | |||||
/// <summary> | |||||
/// The guild is verified. | |||||
/// </summary> | |||||
Verified = 1 << 19, | |||||
/// <summary> | |||||
/// The guild has access to set 384kbps bitrate in voice (previously VIP voice servers). | |||||
/// </summary> | |||||
VIPRegions = 1 << 20, | |||||
/// <summary> | |||||
/// The guild has enabled the welcome screen. | |||||
/// </summary> | |||||
WelcomeScreenEnabled = 1 << 21, | |||||
} | |||||
} |
@@ -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 | |||||
{ | |||||
/// <summary> | |||||
/// Gets the flags of recognized features for this guild. | |||||
/// </summary> | |||||
public GuildFeature Value { get; } | |||||
/// <summary> | |||||
/// Gets a collection of experimental features for this guild. | |||||
/// </summary> | |||||
public IReadOnlyCollection<string> 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<GuildFeature>(); | |||||
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."); | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -207,12 +207,12 @@ namespace Discord | |||||
/// </returns> | /// </returns> | ||||
IReadOnlyCollection<ICustomSticker> Stickers { get; } | IReadOnlyCollection<ICustomSticker> Stickers { get; } | ||||
/// <summary> | /// <summary> | ||||
/// Gets a collection of all extra features added to this guild. | |||||
/// Gets the features for this guild. | |||||
/// </summary> | /// </summary> | ||||
/// <returns> | /// <returns> | ||||
/// A read-only collection of enabled features in this guild. | |||||
/// A flags enum containing all the features for the guild. | |||||
/// </returns> | /// </returns> | ||||
IReadOnlyCollection<string> Features { get; } | |||||
GuildFeatures Features { get; } | |||||
/// <summary> | /// <summary> | ||||
/// Gets a collection of all roles in this guild. | /// Gets a collection of all roles in this guild. | ||||
/// </summary> | /// </summary> | ||||
@@ -35,7 +35,7 @@ namespace Discord.API | |||||
[JsonProperty("emojis")] | [JsonProperty("emojis")] | ||||
public Emoji[] Emojis { get; set; } | public Emoji[] Emojis { get; set; } | ||||
[JsonProperty("features")] | [JsonProperty("features")] | ||||
public string[] Features { get; set; } | |||||
public GuildFeatures Features { get; set; } | |||||
[JsonProperty("mfa_level")] | [JsonProperty("mfa_level")] | ||||
public MfaLevel MfaLevel { get; set; } | public MfaLevel MfaLevel { get; set; } | ||||
[JsonProperty("application_id")] | [JsonProperty("application_id")] | ||||
@@ -11,13 +11,14 @@ namespace Discord.Rest | |||||
public static async Task<Model> CreateThreadAsync(BaseDiscordClient client, ITextChannel channel, string name, ThreadType type = ThreadType.PublicThread, | public static async Task<Model> CreateThreadAsync(BaseDiscordClient client, ITextChannel channel, string name, ThreadType type = ThreadType.PublicThread, | ||||
ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, RequestOptions options = null) | 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)); | 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)); | 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)); | throw new ArgumentException($"The guild {channel.Guild.Name} does not have the PRIVATE_THREADS feature!", nameof(type)); | ||||
var args = new StartThreadParams | var args = new StartThreadParams | ||||
@@ -22,7 +22,6 @@ namespace Discord.Rest | |||||
private ImmutableDictionary<ulong, RestRole> _roles; | private ImmutableDictionary<ulong, RestRole> _roles; | ||||
private ImmutableArray<GuildEmote> _emotes; | private ImmutableArray<GuildEmote> _emotes; | ||||
private ImmutableArray<CustomSticker> _stickers; | private ImmutableArray<CustomSticker> _stickers; | ||||
private ImmutableArray<string> _features; | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public string Name { get; private set; } | public string Name { get; private set; } | ||||
@@ -90,9 +89,10 @@ namespace Discord.Rest | |||||
public NsfwLevel NsfwLevel { get; private set; } | public NsfwLevel NsfwLevel { get; private set; } | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public bool IsBoostProgressBarEnabled { get; private set; } | public bool IsBoostProgressBarEnabled { get; private set; } | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public CultureInfo PreferredCulture { get; private set; } | public CultureInfo PreferredCulture { get; private set; } | ||||
/// <inheritdoc /> | |||||
public GuildFeatures Features { get; private set; } | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | ||||
@@ -118,8 +118,6 @@ namespace Discord.Rest | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public IReadOnlyCollection<GuildEmote> Emotes => _emotes; | public IReadOnlyCollection<GuildEmote> Emotes => _emotes; | ||||
public IReadOnlyCollection<CustomSticker> Stickers => _stickers; | public IReadOnlyCollection<CustomSticker> Stickers => _stickers; | ||||
/// <inheritdoc /> | |||||
public IReadOnlyCollection<string> Features => _features; | |||||
internal RestGuild(BaseDiscordClient client, ulong id) | internal RestGuild(BaseDiscordClient client, ulong id) | ||||
: base(client, id) | : base(client, id) | ||||
@@ -185,10 +183,7 @@ namespace Discord.Rest | |||||
else | else | ||||
_emotes = ImmutableArray.Create<GuildEmote>(); | _emotes = ImmutableArray.Create<GuildEmote>(); | ||||
if (model.Features != null) | |||||
_features = model.Features.ToImmutableArray(); | |||||
else | |||||
_features = ImmutableArray.Create<string>(); | |||||
Features = model.Features; | |||||
var roles = ImmutableDictionary.CreateBuilder<ulong, RestRole>(); | var roles = ImmutableDictionary.CreateBuilder<ulong, RestRole>(); | ||||
if (model.Roles != null) | if (model.Roles != null) | ||||
@@ -18,6 +18,12 @@ namespace Discord.Rest | |||||
{ | { | ||||
var args = new RoleProperties(); | var args = new RoleProperties(); | ||||
func(args); | func(args); | ||||
if (args.Icon.IsSpecified) | |||||
{ | |||||
role.Guild.Features.EnsureFeature(GuildFeature.RoleIcons); | |||||
} | |||||
var apiArgs = new API.Rest.ModifyGuildRoleParams | var apiArgs = new API.Rest.ModifyGuildRoleParams | ||||
{ | { | ||||
Color = args.Color.IsSpecified ? args.Color.Value.RawValue : Optional.Create<uint>(), | Color = args.Color.IsSpecified ? args.Color.Value.RawValue : Optional.Create<uint>(), | ||||
@@ -87,6 +87,8 @@ namespace Discord.Net.Converters | |||||
return MessageComponentConverter.Instance; | return MessageComponentConverter.Instance; | ||||
if (type == typeof(API.Interaction)) | if (type == typeof(API.Interaction)) | ||||
return InteractionConverter.Instance; | return InteractionConverter.Instance; | ||||
if (type == typeof(GuildFeatures)) | |||||
return GuildFeaturesConverter.Instance; | |||||
//Entities | //Entities | ||||
var typeInfo = type.GetTypeInfo(); | var typeInfo = type.GetTypeInfo(); | ||||
@@ -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<string[]>(); | |||||
GuildFeature features = GuildFeature.None; | |||||
List<string> 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(); | |||||
} | |||||
} | |||||
} |
@@ -572,6 +572,8 @@ namespace Discord.WebSocket | |||||
{ | { | ||||
if (ConnectionState == ConnectionState.Connected) | if (ConnectionState == ConnectionState.Connected) | ||||
{ | { | ||||
EnsureGatewayIntent(GatewayIntents.GuildMembers); | |||||
//Race condition leads to guilds being requested twice, probably okay | //Race condition leads to guilds being requested twice, probably okay | ||||
await ProcessUserDownloadsAsync(guilds.Select(x => GetGuild(x.Id)).Where(x => x != null)).ConfigureAwait(false); | 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); | channel.Recipient.GlobalUser.RemoveRef(this); | ||||
} | } | ||||
internal void EnsureGatewayIntent(GatewayIntents intents) | |||||
{ | |||||
if (!_gatewayIntents.HasFlag(intents)) | |||||
{ | |||||
var vals = Enum.GetValues(typeof(GatewayIntents)).Cast<GatewayIntents>(); | |||||
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) | private async Task GuildAvailableAsync(SocketGuild guild) | ||||
{ | { | ||||
if (!guild.IsConnected) | if (!guild.IsConnected) | ||||
@@ -42,7 +42,6 @@ namespace Discord.WebSocket | |||||
private ConcurrentDictionary<ulong, SocketCustomSticker> _stickers; | private ConcurrentDictionary<ulong, SocketCustomSticker> _stickers; | ||||
private ImmutableArray<GuildEmote> _emotes; | private ImmutableArray<GuildEmote> _emotes; | ||||
private ImmutableArray<string> _features; | |||||
private AudioClient _audioClient; | private AudioClient _audioClient; | ||||
#pragma warning restore IDISP002, IDISP006 | #pragma warning restore IDISP002, IDISP006 | ||||
@@ -129,6 +128,8 @@ namespace Discord.WebSocket | |||||
public CultureInfo PreferredCulture { get; private set; } | public CultureInfo PreferredCulture { get; private set; } | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public bool IsBoostProgressBarEnabled { get; private set; } | public bool IsBoostProgressBarEnabled { get; private set; } | ||||
/// <inheritdoc /> | |||||
public GuildFeatures Features { get; private set; } | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | ||||
@@ -333,8 +334,6 @@ namespace Discord.WebSocket | |||||
/// </summary> | /// </summary> | ||||
public IReadOnlyCollection<SocketCustomSticker> Stickers | public IReadOnlyCollection<SocketCustomSticker> Stickers | ||||
=> _stickers.Select(x => x.Value).ToImmutableArray(); | => _stickers.Select(x => x.Value).ToImmutableArray(); | ||||
/// <inheritdoc /> | |||||
public IReadOnlyCollection<string> Features => _features; | |||||
/// <summary> | /// <summary> | ||||
/// Gets a collection of users in this guild. | /// Gets a collection of users in this guild. | ||||
/// </summary> | /// </summary> | ||||
@@ -370,7 +369,6 @@ namespace Discord.WebSocket | |||||
{ | { | ||||
_audioLock = new SemaphoreSlim(1, 1); | _audioLock = new SemaphoreSlim(1, 1); | ||||
_emotes = ImmutableArray.Create<GuildEmote>(); | _emotes = ImmutableArray.Create<GuildEmote>(); | ||||
_features = ImmutableArray.Create<string>(); | |||||
} | } | ||||
internal static SocketGuild Create(DiscordSocketClient discord, ClientState state, ExtendedModel model) | internal static SocketGuild Create(DiscordSocketClient discord, ClientState state, ExtendedModel model) | ||||
{ | { | ||||
@@ -508,10 +506,7 @@ namespace Discord.WebSocket | |||||
else | else | ||||
_emotes = ImmutableArray.Create<GuildEmote>(); | _emotes = ImmutableArray.Create<GuildEmote>(); | ||||
if (model.Features != null) | |||||
_features = model.Features.ToImmutableArray(); | |||||
else | |||||
_features = ImmutableArray.Create<string>(); | |||||
Features = model.Features; | |||||
var roles = new ConcurrentDictionary<ulong, SocketRole>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Roles.Length * 1.05)); | var roles = new ConcurrentDictionary<ulong, SocketRole>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Roles.Length * 1.05)); | ||||
if (model.Roles != null) | if (model.Roles != null) | ||||