| @@ -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) | ||||