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