| @@ -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> | |||
| IReadOnlyCollection<ICustomSticker> Stickers { get; } | |||
| /// <summary> | |||
| /// Gets a collection of all extra features added to this guild. | |||
| /// Gets the features for this guild. | |||
| /// </summary> | |||
| /// <returns> | |||
| /// A read-only collection of enabled features in this guild. | |||
| /// A flags enum containing all the features for the guild. | |||
| /// </returns> | |||
| IReadOnlyCollection<string> Features { get; } | |||
| GuildFeatures Features { get; } | |||
| /// <summary> | |||
| /// Gets a collection of all roles in this guild. | |||
| /// </summary> | |||
| @@ -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")] | |||
| @@ -11,13 +11,14 @@ namespace Discord.Rest | |||
| 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) | |||
| { | |||
| 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 | |||
| @@ -22,7 +22,6 @@ namespace Discord.Rest | |||
| private ImmutableDictionary<ulong, RestRole> _roles; | |||
| private ImmutableArray<GuildEmote> _emotes; | |||
| private ImmutableArray<CustomSticker> _stickers; | |||
| private ImmutableArray<string> _features; | |||
| /// <inheritdoc /> | |||
| public string Name { get; private set; } | |||
| @@ -90,9 +89,10 @@ namespace Discord.Rest | |||
| public NsfwLevel NsfwLevel { get; private set; } | |||
| /// <inheritdoc /> | |||
| public bool IsBoostProgressBarEnabled { get; private set; } | |||
| /// <inheritdoc /> | |||
| public CultureInfo PreferredCulture { get; private set; } | |||
| /// <inheritdoc /> | |||
| public GuildFeatures Features { get; private set; } | |||
| /// <inheritdoc /> | |||
| public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | |||
| @@ -118,8 +118,6 @@ namespace Discord.Rest | |||
| /// <inheritdoc /> | |||
| public IReadOnlyCollection<GuildEmote> Emotes => _emotes; | |||
| public IReadOnlyCollection<CustomSticker> Stickers => _stickers; | |||
| /// <inheritdoc /> | |||
| public IReadOnlyCollection<string> Features => _features; | |||
| internal RestGuild(BaseDiscordClient client, ulong id) | |||
| : base(client, id) | |||
| @@ -185,10 +183,7 @@ namespace Discord.Rest | |||
| else | |||
| _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>(); | |||
| if (model.Roles != null) | |||
| @@ -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<uint>(), | |||
| @@ -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(); | |||
| @@ -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) | |||
| { | |||
| 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<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) | |||
| { | |||
| if (!guild.IsConnected) | |||
| @@ -42,7 +42,6 @@ namespace Discord.WebSocket | |||
| private ConcurrentDictionary<ulong, SocketCustomSticker> _stickers; | |||
| private ImmutableArray<GuildEmote> _emotes; | |||
| private ImmutableArray<string> _features; | |||
| private AudioClient _audioClient; | |||
| #pragma warning restore IDISP002, IDISP006 | |||
| @@ -129,6 +128,8 @@ namespace Discord.WebSocket | |||
| public CultureInfo PreferredCulture { get; private set; } | |||
| /// <inheritdoc /> | |||
| public bool IsBoostProgressBarEnabled { get; private set; } | |||
| /// <inheritdoc /> | |||
| public GuildFeatures Features { get; private set; } | |||
| /// <inheritdoc /> | |||
| public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | |||
| @@ -333,8 +334,6 @@ namespace Discord.WebSocket | |||
| /// </summary> | |||
| public IReadOnlyCollection<SocketCustomSticker> Stickers | |||
| => _stickers.Select(x => x.Value).ToImmutableArray(); | |||
| /// <inheritdoc /> | |||
| public IReadOnlyCollection<string> Features => _features; | |||
| /// <summary> | |||
| /// Gets a collection of users in this guild. | |||
| /// </summary> | |||
| @@ -370,7 +369,6 @@ namespace Discord.WebSocket | |||
| { | |||
| _audioLock = new SemaphoreSlim(1, 1); | |||
| _emotes = ImmutableArray.Create<GuildEmote>(); | |||
| _features = ImmutableArray.Create<string>(); | |||
| } | |||
| internal static SocketGuild Create(DiscordSocketClient discord, ClientState state, ExtendedModel model) | |||
| { | |||
| @@ -508,10 +506,7 @@ namespace Discord.WebSocket | |||
| else | |||
| _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)); | |||
| if (model.Roles != null) | |||