diff --git a/docs/guides/guild_events/creating-guild-events.md b/docs/guides/guild_events/creating-guild-events.md new file mode 100644 index 000000000..64ac0de9b --- /dev/null +++ b/docs/guides/guild_events/creating-guild-events.md @@ -0,0 +1,31 @@ +--- +uid: Guides.GuildEvents.Creating +title: Creating Guild Events +--- + +# Creating guild events + +You can create new guild events by using the `CreateEventAsync` function on a guild. + +### Parameters + +| Name | Type | Summary | +| ------------- | --------------------------------- | ---------------------------------------------------------------------------- | +| name | `string` | Sets the name of the event. | +| startTime | `DateTimeOffset` | Sets the start time of the event. | +| type | `GuildScheduledEventType` | Sets the type of the event. | +| privacyLevel? | `GuildScheduledEventPrivacyLevel` | Sets the privacy level of the event | +| description? | `string` | Sets the description of the event. | +| endTime? | `DateTimeOffset?` | Sets the end time of the event. | +| channelId? | `ulong?` | Sets the channel id of the event, only valid on stage or voice channel types | +| location? | `string` | Sets the location of the event, only valid on external types | + +Lets create a basic test event. + +```cs +var guild = client.GetGuild(guildId); + +var guildEvent = await guild.CreateEventAsync("test event", DateTimeOffset.UtcNow.AddDays(1), GuildScheduledEventType.External, endTime: DateTimeOffset.UtcNow.AddDays(2), location: "Space"); +``` + +This code will create an event that lasts a day and starts tomorrow. It will be an external event thats in space. diff --git a/docs/guides/guild_events/getting-event-users.md b/docs/guides/guild_events/getting-event-users.md new file mode 100644 index 000000000..f4b5388a0 --- /dev/null +++ b/docs/guides/guild_events/getting-event-users.md @@ -0,0 +1,16 @@ +--- +uid: Guides.GuildEvents.GettingUsers +title: Getting Guild Event Users +--- + +# Getting Event Users + +You can get a collection of users who are currently interested in the event by calling `GetUsersAsync`. This method works like any other get users method as in it returns an async enumerable. This method also supports pagination by user id. + +```cs +// get all users and flatten the result into one collection. +var users = await event.GetUsersAsync().FlattenAsync(); + +// get users around the 613425648685547541 id. +var aroundUsers = await event.GetUsersAsync(613425648685547541, Direction.Around).FlattenAsync(); +``` diff --git a/docs/guides/guild_events/intro.md b/docs/guides/guild_events/intro.md new file mode 100644 index 000000000..b60a8c70d --- /dev/null +++ b/docs/guides/guild_events/intro.md @@ -0,0 +1,41 @@ +--- +uid: Guides.GuildEvents.Intro +title: Introduction to Guild Events +--- + +# Guild Events + +Guild events are a way to host events within a guild. They offer alot of features and flexibility. + +## Getting started with guild events + +You can access any events within a guild by calling `GetEventsAsync` on a guild. + +```cs +var guildEvents = await guild.GetEventsAsync(); +``` + +If your working with socket guilds you can just use the `Events` property: + +```cs +var guildEvents = guild.Events; +``` + +There are also new gateway events that you can hook to receive guild scheduled events on. + +```cs +// Fired when a guild event is cancelled. +client.GuildScheduledEventCancelled += ... + +// Fired when a guild event is completed. +client.GuildScheduledEventCompleted += ... + +// Fired when a guild event is started. +client.GuildScheduledEventStarted += ... + +// Fired when a guild event is created. +client.GuildScheduledEventCreated += ... + +// Fired when a guild event is updated. +client.GuildScheduledEventUpdated += ... +``` diff --git a/docs/guides/guild_events/modifying-events.md b/docs/guides/guild_events/modifying-events.md new file mode 100644 index 000000000..05e14ec98 --- /dev/null +++ b/docs/guides/guild_events/modifying-events.md @@ -0,0 +1,23 @@ +--- +uid: Guides.GuildEvents.Modifying +title: Modifying Guild Events +--- + +# Modifying Events + +You can modify events using the `ModifyAsync` method to modify the event, heres the properties you can modify: + +| Name | Type | Description | +| ------------ | --------------------------------- | -------------------------------------------- | +| ChannelId | `ulong?` | Gets or sets the channel id of the event. | +| string | `string` | Gets or sets the location of this event. | +| Name | `string` | Gets or sets the name of the event. | +| PrivacyLevel | `GuildScheduledEventPrivacyLevel` | Gets or sets the privacy level of the event. | +| StartTime | `DateTimeOffset` | Gets or sets the start time of the event. | +| EndTime | `DateTimeOffset` | Gets or sets the end time of the event. | +| Description | `string` | Gets or sets the description of the event. | +| Type | `GuildScheduledEventType` | Gets or sets the type of the event. | +| Status | `GuildScheduledEventStatus` | Gets or sets the status of the event. | + +> [!NOTE] +> All of these properties are optional. diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index 603258c33..6bd2d269a 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -1,5 +1,15 @@ - name: Introduction topicUid: Guides.Introduction +- name: "Working with Guild Events" + items: + - name: Introduction + topicUid: Guides.GuildEvents.Intro + - name: Creating Events + topicUid: Guides.GuildEvents.Creating + - name: Getting Event Users + topicUid: Guides.GuildEvents.GettingUsers + - name: Modifying Events + topicUid: Guides.GuildEvents.Modifying - name: Working with Slash commands items: - name: Introduction diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index 621c8184c..03994a3c7 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -94,6 +94,13 @@ namespace Discord /// The maximum number of users that can be gotten per-batch. /// public const int MaxUsersPerBatch = 1000; + /// + /// Returns the max users allowed to be in a request for guild event users. + /// + /// + /// The maximum number of users that can be gotten per-batch. + /// + public const int MaxGuildEventUsersPerBatch = 100; /// /// Returns the max guilds allowed to be in a request. /// diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventPrivacyLevel.cs b/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventPrivacyLevel.cs new file mode 100644 index 000000000..87881104c --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventPrivacyLevel.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents the privacy level of a guild scheduled event. + /// + public enum GuildScheduledEventPrivacyLevel + { + /// + /// The scheduled event is public and available in discovery. + /// + [Obsolete("This event type isn't supported yet! check back later.", true)] + Public = 1, + + /// + /// The scheduled event is only accessible to guild members. + /// + Private = 2, + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventStatus.cs b/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventStatus.cs new file mode 100644 index 000000000..6e3aa1ab3 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventStatus.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents the status of a guild event. + /// + public enum GuildScheduledEventStatus + { + /// + /// The event is scheduled for a set time. + /// + Scheduled = 1, + + /// + /// The event has started. + /// + Active = 2, + + /// + /// The event was completed. + /// + Completed = 3, + + /// + /// The event was canceled. + /// + Cancelled = 4, + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventType.cs b/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventType.cs new file mode 100644 index 000000000..ad741eee1 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventType.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents the type of a guild scheduled event. + /// + public enum GuildScheduledEventType + { + /// + /// The event doesn't have a set type. + /// + None = 0, + + /// + /// The event is set in a stage channel. + /// + Stage = 1, + + /// + /// The event is set in a voice channel. + /// + Voice = 2, + + /// + /// The event is set for somewhere externally from discord. + /// + External = 3, + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventsProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventsProperties.cs new file mode 100644 index 000000000..a3fd729e5 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventsProperties.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Provides properties that are used to modify an with the specified changes. + /// + public class GuildScheduledEventsProperties + { + /// + /// Gets or sets the channel id of the event. + /// + public Optional ChannelId { get; set; } + + /// + /// Gets or sets the location of this event. + /// + public Optional Location { get; set; } + + /// + /// Gets or sets the name of the event. + /// + public Optional Name { get; set; } + + /// + /// Gets or sets the privacy level of the event. + /// + public Optional PrivacyLevel { get; set; } + + /// + /// Gets or sets the start time of the event. + /// + public Optional StartTime { get; set; } + /// + /// Gets or sets the end time of the event. + /// + public Optional EndTime { get; set; } + + /// + /// Gets or sets the description of the event. + /// + public Optional Description { get; set; } + + /// + /// Gets or sets the type of the event. + /// + public Optional Type { get; set; } + + /// + /// Gets or sets the status of the event. + /// + public Optional Status { get; set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index fa76cc360..ebf2ccd4a 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -1056,6 +1056,58 @@ namespace Discord /// Task DeleteStickerAsync(ICustomSticker sticker, RequestOptions options = null); + /// + /// Gets a event within this guild. + /// + /// The id of the event. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. + /// + Task GetEventAsync(ulong id, RequestOptions options = null); + + /// + /// Gets a collection of events within this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. + /// + Task> GetEventsAsync(RequestOptions options = null); + + /// + /// Creates an event within this guild. + /// + /// The name of the event. + /// The privacy level of the event. + /// The start time of the event. + /// The type of the event. + /// The description of the event. + /// The end time of the event. + /// + /// The channel id of the event. + /// + /// The event must have a type of or + /// in order to use this property. + /// + /// + /// A collection of speakers for the event. + /// The location of the event; links are supported + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous create operation. + /// + Task CreateEventAsync( + string name, + DateTimeOffset startTime, + GuildScheduledEventType type, + GuildScheduledEventPrivacyLevel privacyLevel = GuildScheduledEventPrivacyLevel.Private, + string description = null, + DateTimeOffset? endTime = null, + ulong? channelId = null, + string location = null, + RequestOptions options = null); + /// /// Gets this guilds application commands. /// diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs b/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs new file mode 100644 index 000000000..e50f4cc2b --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a generic guild scheduled event. + /// + public interface IGuildScheduledEvent : IEntity + { + /// + /// Gets the guild this event is scheduled in. + /// + IGuild Guild { get; } + + /// + /// Gets the optional channel id where this event will be hosted. + /// + ulong? ChannelId { get; } + + /// + /// Gets the user who created the event. + /// + IUser Creator { get; } + + /// + /// Gets the name of the event. + /// + string Name { get; } + + /// + /// Gets the description of the event. + /// + /// + /// This field is when the event doesn't have a discription. + /// + string Description { get; } + + /// + /// Gets the start time of the event. + /// + DateTimeOffset StartTime { get; } + + /// + /// Gets the optional end time of the event. + /// + DateTimeOffset? EndTime { get; } + + /// + /// Gets the privacy level of the event. + /// + GuildScheduledEventPrivacyLevel PrivacyLevel { get; } + + /// + /// Gets the status of the event. + /// + GuildScheduledEventStatus Status { get; } + + /// + /// Gets the type of the event. + /// + GuildScheduledEventType Type { get; } + + /// + /// Gets the optional entity id of the event. The "entity" of the event + /// can be a stage instance event as is seperate from . + /// + ulong? EntityId { get; } + + /// + /// Gets the location of the event if the is external. + /// + string Location { get; } + + /// + /// Gets the user count of the event. + /// + int? UserCount { get; } + + /// + /// Starts the event. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous start operation. + /// + Task StartAsync(RequestOptions options = null); + /// + /// Ends or canceles the event. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous end operation. + /// + Task EndAsync(RequestOptions options = null); + + /// + /// Modifies the guild event. + /// + /// The delegate containing the properties to modify the event with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + Task ModifyAsync(Action func, RequestOptions options = null); + + /// + /// Deletes the current event. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous delete operation. + /// + Task DeleteAsync(RequestOptions options = null); + + /// + /// Gets a collection of N users interested in the event. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual messages as a + /// collection. + /// + /// This method will attempt to fetch all users that are interested in the event. + /// The library will attempt to split up the requests according to and . + /// In other words, if there are 300 users, and the constant + /// is 100, the request will be split into 3 individual requests; thus returning 3 individual asynchronous + /// responses, hence the need of flattening. + /// + /// The options to be used when sending the request. + /// + /// Paged collection of users. + /// + IAsyncEnumerable> GetUsersAsync(RequestOptions options = null); + + /// + /// Gets a collection of N users interested in the event. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual users as a + /// collection. + /// + /// + /// Do not fetch too many users at once! This may cause unwanted preemptive rate limit or even actual + /// rate limit, causing your bot to freeze! + /// + /// This method will attempt to fetch the number of users specified under around + /// the user depending on the . The library will + /// attempt to split up the requests according to your and + /// . In other words, should the user request 500 users, + /// and the constant is 100, the request will + /// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need + /// of flattening. + /// + /// The ID of the starting user to get the users from. + /// The direction of the users to be gotten from. + /// The numbers of users to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of users. + /// + IAsyncEnumerable> GetUsersAsync(ulong fromUserId, Direction dir, int limit = DiscordConfig.MaxGuildEventUsersPerBatch, RequestOptions options = null); + } +} diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs index 746981c6e..448fd20b9 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs @@ -185,10 +185,14 @@ namespace Discord /// UseApplicationCommands = 0x80_00_00_00, /// - /// Allows for requesting to speak in stage channels. (This permission is under active development and may be changed or removed.). + /// Allows for requesting to speak in stage channels. /// RequestToSpeak = 0x01_00_00_00_00, /// + /// Allows for creating, editing, and deleting guild scheduled events. + /// + ManageEvents = 0x02_00_00_00_00, + /// /// Allows for deleting and archiving threads, and viewing all private threads. /// /// diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs index 31f74ea22..8a4ad2189 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; namespace Discord { @@ -87,6 +88,8 @@ namespace Discord public bool UseApplicationCommands => Permissions.GetValue(RawValue, GuildPermission.UseApplicationCommands); /// If true, a user may request to speak in stage channels. public bool RequestToSpeak => Permissions.GetValue(RawValue, GuildPermission.RequestToSpeak); + /// If true, a user may create, edit, and delete events. + public bool ManageEvents => Permissions.GetValue(RawValue, GuildPermission.ManageEvents); /// If true, a user may manage threads in this guild. public bool ManageThreads => Permissions.GetValue(RawValue, GuildPermission.ManageThreads); /// If true, a user may create public threads in this guild. @@ -140,6 +143,7 @@ namespace Discord bool? manageEmojisAndStickers = null, bool? useApplicationCommands = null, bool? requestToSpeak = null, + bool? manageEvents = null, bool? manageThreads = null, bool? createPublicThreads = null, bool? createPrivateThreads = null, @@ -182,6 +186,7 @@ namespace Discord Permissions.SetValue(ref value, manageEmojisAndStickers, GuildPermission.ManageEmojisAndStickers); Permissions.SetValue(ref value, useApplicationCommands, GuildPermission.UseApplicationCommands); Permissions.SetValue(ref value, requestToSpeak, GuildPermission.RequestToSpeak); + Permissions.SetValue(ref value, manageEvents, GuildPermission.ManageEvents); Permissions.SetValue(ref value, manageThreads, GuildPermission.ManageThreads); Permissions.SetValue(ref value, createPublicThreads, GuildPermission.CreatePublicThreads); Permissions.SetValue(ref value, createPrivateThreads, GuildPermission.CreatePrivateThreads); @@ -227,6 +232,7 @@ namespace Discord bool manageEmojisAndStickers = false, bool useApplicationCommands = false, bool requestToSpeak = false, + bool manageEvents = false, bool manageThreads = false, bool createPublicThreads = false, bool createPrivateThreads = false, @@ -267,6 +273,7 @@ namespace Discord manageEmojisAndStickers: manageEmojisAndStickers, useApplicationCommands: useApplicationCommands, requestToSpeak: requestToSpeak, + manageEvents: manageEvents, manageThreads: manageThreads, createPublicThreads: createPublicThreads, createPrivateThreads: createPrivateThreads, @@ -310,6 +317,7 @@ namespace Discord bool? manageEmojisAndStickers = null, bool? useApplicationCommands = null, bool? requestToSpeak = null, + bool? manageEvents = null, bool? manageThreads = null, bool? createPublicThreads = null, bool? createPrivateThreads = null, @@ -320,7 +328,7 @@ namespace Discord viewAuditLog, viewGuildInsights, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, prioritySpeaker, stream, changeNickname, manageNicknames, manageRoles, manageWebhooks, manageEmojisAndStickers, - useApplicationCommands, requestToSpeak, manageThreads, createPublicThreads, createPrivateThreads, useExternalStickers, sendMessagesInThreads, + useApplicationCommands, requestToSpeak, manageEvents, manageThreads, createPublicThreads, createPrivateThreads, useExternalStickers, sendMessagesInThreads, startEmbeddedActivities); /// @@ -351,6 +359,18 @@ namespace Discord return perms; } + internal void Ensure(GuildPermission permissions) + { + if (!Has(permissions)) + { + var vals = Enum.GetValues(typeof(GuildPermission)).Cast(); + var currentValues = RawValue; + var missingValues = vals.Where(x => permissions.HasFlag(x) && !Permissions.GetValue(currentValues, x)); + + throw new InvalidOperationException($"Missing required guild permission{(missingValues.Count() > 1 ? "s" : "")} {string.Join(", ", missingValues.Select(x => x.ToString()))} in order to execute this operation."); + } + } + public override string ToString() => RawValue.ToString(); private string DebuggerDisplay => $"{string.Join(", ", ToList())}"; } diff --git a/src/Discord.Net.Core/GatewayIntents.cs b/src/Discord.Net.Core/GatewayIntents.cs index fb0aac6bc..f2a99e44c 100644 --- a/src/Discord.Net.Core/GatewayIntents.cs +++ b/src/Discord.Net.Core/GatewayIntents.cs @@ -39,13 +39,15 @@ namespace Discord DirectMessageReactions = 1 << 13, /// This intent includes TYPING_START DirectMessageTyping = 1 << 14, + /// This intent includes GUILD_SCHEDULED_EVENT_CREATE, GUILD_SCHEDULED_EVENT_UPDATE, GUILD_SCHEDULED_EVENT_DELETE, GUILD_SCHEDULED_EVENT_USER_ADD, GUILD_SCHEDULED_EVENT_USER_REMOVE + GuildScheduledEvents = 1 << 16, /// /// This intent includes all but and /// which are privileged and must be enabled in the Developer Portal. /// AllUnprivileged = Guilds | GuildBans | GuildEmojis | GuildIntegrations | GuildWebhooks | GuildInvites | GuildVoiceStates | GuildMessages | GuildMessageReactions | GuildMessageTyping | DirectMessages | - DirectMessageReactions | DirectMessageTyping, + DirectMessageReactions | DirectMessageTyping | GuildScheduledEvents, /// /// This intent includes all of them, including privileged ones. /// diff --git a/src/Discord.Net.Rest/API/Common/GuildScheduledEvent.cs b/src/Discord.Net.Rest/API/Common/GuildScheduledEvent.cs new file mode 100644 index 000000000..338c24dc9 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GuildScheduledEvent.cs @@ -0,0 +1,43 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class GuildScheduledEvent + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("channel_id")] + public Optional ChannelId { get; set; } + [JsonProperty("creator_id")] + public Optional CreatorId { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("description")] + public Optional Description { get; set; } + [JsonProperty("scheduled_start_time")] + public DateTimeOffset ScheduledStartTime { get; set; } + [JsonProperty("scheduled_end_time")] + public DateTimeOffset? ScheduledEndTime { get; set; } + [JsonProperty("privacy_level")] + public GuildScheduledEventPrivacyLevel PrivacyLevel { get; set; } + [JsonProperty("status")] + public GuildScheduledEventStatus Status { get; set; } + [JsonProperty("entity_type")] + public GuildScheduledEventType EntityType { get; set; } + [JsonProperty("entity_id")] + public ulong? EntityId { get; set; } + [JsonProperty("entity_metadata")] + public GuildScheduledEventEntityMetadata EntityMetadata { get; set; } + [JsonProperty("creator")] + public Optional Creator { get; set; } + [JsonProperty("user_count")] + public Optional UserCount { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/GuildScheduledEventEntityMetadata.cs b/src/Discord.Net.Rest/API/Common/GuildScheduledEventEntityMetadata.cs new file mode 100644 index 000000000..1db38c0ae --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GuildScheduledEventEntityMetadata.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class GuildScheduledEventEntityMetadata + { + [JsonProperty("location")] + public Optional Location { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/GuildScheduledEventUser.cs b/src/Discord.Net.Rest/API/Common/GuildScheduledEventUser.cs new file mode 100644 index 000000000..1b0b93763 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GuildScheduledEventUser.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class GuildScheduledEventUser + { + [JsonProperty("user")] + public User User { get; set; } + [JsonProperty("member")] + public Optional Member { get; set; } + [JsonProperty("guild_scheduled_event_id")] + public ulong GuildScheduledEventId { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/CreateGuildScheduledEventParams.cs b/src/Discord.Net.Rest/API/Rest/CreateGuildScheduledEventParams.cs new file mode 100644 index 000000000..a207d3374 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/CreateGuildScheduledEventParams.cs @@ -0,0 +1,29 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Rest +{ + internal class CreateGuildScheduledEventParams + { + [JsonProperty("channel_id")] + public Optional ChannelId { get; set; } + [JsonProperty("entity_metadata")] + public Optional EntityMetadata { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("privacy_level")] + public GuildScheduledEventPrivacyLevel PrivacyLevel { get; set; } + [JsonProperty("scheduled_start_time")] + public DateTimeOffset StartTime { get; set; } + [JsonProperty("scheduled_end_time")] + public Optional EndTime { get; set; } + [JsonProperty("description")] + public Optional Description { get; set; } + [JsonProperty("entity_type")] + public GuildScheduledEventType Type { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/GetEventUsersParams.cs b/src/Discord.Net.Rest/API/Rest/GetEventUsersParams.cs new file mode 100644 index 000000000..db3ac666e --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/GetEventUsersParams.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Rest +{ + internal class GetEventUsersParams + { + public Optional Limit { get; set; } + public Optional RelativeDirection { get; set; } + public Optional RelativeUserId { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildScheduledEventParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildScheduledEventParams.cs new file mode 100644 index 000000000..3d191a0b3 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildScheduledEventParams.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Rest +{ + internal class ModifyGuildScheduledEventParams + { + [JsonProperty("channel_id")] + public Optional ChannelId { get; set; } + [JsonProperty("entity_metadata")] + public Optional EntityMetadata { get; set; } + [JsonProperty("name")] + public Optional Name { get; set; } + [JsonProperty("privacy_level")] + public Optional PrivacyLevel { get; set; } + [JsonProperty("scheduled_start_time")] + public Optional StartTime { get; set; } + [JsonProperty("scheduled_end_time")] + public Optional EndTime { get; set; } + [JsonProperty("description")] + public Optional Description { get; set; } + [JsonProperty("entity_type")] + public Optional Type { get; set; } + [JsonProperty("status")] + public Optional Status { get; set; } + } +} diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index ff150e601..262da65be 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -1909,6 +1909,105 @@ namespace Discord.API } #endregion + #region Guild Events + + public async Task ListGuildScheduledEventsAsync(ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync("GET", () => $"guilds/{guildId}/scheduled-events?with_user_count=true", ids, options: options).ConfigureAwait(false); + } + + public async Task GetGuildScheduledEventAsync(ulong eventId, ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(eventId, 0, nameof(eventId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + + return await NullifyNotFound(SendAsync("GET", () => $"guilds/{guildId}/scheduled-events/{eventId}?with_user_count=true", ids, options: options)).ConfigureAwait(false); + } + + public async Task CreateGuildScheduledEventAsync(CreateGuildScheduledEventParams args, ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + + return await SendJsonAsync("POST", () => $"guilds/{guildId}/scheduled-events", args, ids, options: options).ConfigureAwait(false); + } + + public async Task ModifyGuildScheduledEventAsync(ModifyGuildScheduledEventParams args, ulong eventId, ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(eventId, 0, nameof(eventId)); + Preconditions.NotNull(args, nameof(args)); + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + + return await SendJsonAsync("PATCH", () => $"guilds/{guildId}/scheduled-events/{eventId}", args, ids, options: options).ConfigureAwait(false); + } + + public async Task DeleteGuildScheduledEventAsync(ulong eventId, ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(eventId, 0, nameof(eventId)); + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + + await SendAsync("DELETE", () => $"guilds/{guildId}/scheduled-events/{eventId}", ids, options: options).ConfigureAwait(false); + } + + public async Task GetGuildScheduledEventUsersAsync(ulong eventId, ulong guildId, int limit = 100, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(eventId, 0, nameof(eventId)); + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + + return await SendAsync("GET", () => $"guilds/{guildId}/scheduled-events/{eventId}/users?limit={limit}&with_member=true", ids, options: options).ConfigureAwait(false); + } + + public async Task GetGuildScheduledEventUsersAsync(ulong eventId, ulong guildId, GetEventUsersParams args, RequestOptions options = null) + { + Preconditions.NotEqual(eventId, 0, nameof(eventId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.AtLeast(args.Limit, 0, nameof(args.Limit)); + Preconditions.AtMost(args.Limit, DiscordConfig.MaxMessagesPerBatch, nameof(args.Limit)); + options = RequestOptions.CreateOrClone(options); + + int limit = args.Limit.GetValueOrDefault(DiscordConfig.MaxGuildEventUsersPerBatch); + ulong? relativeId = args.RelativeUserId.IsSpecified ? args.RelativeUserId.Value : (ulong?)null; + var relativeDir = args.RelativeDirection.GetValueOrDefault(Direction.Before) switch + { + Direction.After => "after", + Direction.Around => "around", + _ => "before", + }; + var ids = new BucketIds(guildId: guildId); + Expression> endpoint; + if (relativeId != null) + endpoint = () => $"guilds/{guildId}/scheduled-events/{eventId}/users?with_member=true&limit={limit}&{relativeDir}={relativeId}"; + else + endpoint = () => $"guilds/{guildId}/scheduled-events/{eventId}/users?with_member=true&limit={limit}"; + + return await SendAsync("GET", endpoint, ids, options: options).ConfigureAwait(false); + } + + #endregion + #region Users public async Task GetUserAsync(ulong userId, RequestOptions options = null) { diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 51132d513..2cdbbb7b5 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -649,6 +649,235 @@ namespace Discord.Rest public static async Task DeleteStickerAsync(BaseDiscordClient client, ulong guildId, ISticker sticker, RequestOptions options = null) => await client.ApiClient.DeleteStickerAsync(guildId, sticker.Id, options).ConfigureAwait(false); - #endregion + #endregion + + #region Events + + public static async Task> GetEventUsersAsync(BaseDiscordClient client, IGuildScheduledEvent guildEvent, int limit = 100, RequestOptions options = null) + { + var models = await client.ApiClient.GetGuildScheduledEventUsersAsync(guildEvent.Id, guildEvent.Guild.Id, limit, options).ConfigureAwait(false); + + return models.Select(x => RestUser.Create(client, guildEvent.Guild, x)).ToImmutableArray(); + } + + public static IAsyncEnumerable> GetEventUsersAsync(BaseDiscordClient client, IGuildScheduledEvent guildEvent, + ulong? fromUserId, int? limit, RequestOptions options) + { + return new PagedAsyncEnumerable( + DiscordConfig.MaxGuildEventUsersPerBatch, + async (info, ct) => + { + var args = new GetEventUsersParams + { + Limit = info.PageSize, + RelativeDirection = Direction.After, + }; + if (info.Position != null) + args.RelativeUserId = info.Position.Value; + var models = await client.ApiClient.GetGuildScheduledEventUsersAsync(guildEvent.Id, guildEvent.Guild.Id, args, options).ConfigureAwait(false); + return models + .Select(x => RestUser.Create(client, guildEvent.Guild, x)) + .ToImmutableArray(); + }, + nextPage: (info, lastPage) => + { + if (lastPage.Count != DiscordConfig.MaxGuildEventUsersPerBatch) + return false; + info.Position = lastPage.Max(x => x.Id); + return true; + }, + start: fromUserId, + count: limit + ); + } + + public static IAsyncEnumerable> GetEventUsersAsync(BaseDiscordClient client, IGuildScheduledEvent guildEvent, + ulong? fromUserId, Direction dir, int limit, RequestOptions options = null) + { + if (dir == Direction.Around && limit > DiscordConfig.MaxMessagesPerBatch) + { + int around = limit / 2; + if (fromUserId.HasValue) + return GetEventUsersAsync(client, guildEvent, fromUserId.Value + 1, Direction.Before, around + 1, options) //Need to include the message itself + .Concat(GetEventUsersAsync(client, guildEvent, fromUserId, Direction.After, around, options)); + else //Shouldn't happen since there's no public overload for ulong? and Direction + return GetEventUsersAsync(client, guildEvent, null, Direction.Before, around + 1, options); + } + + return new PagedAsyncEnumerable( + DiscordConfig.MaxGuildEventUsersPerBatch, + async (info, ct) => + { + var args = new GetEventUsersParams + { + RelativeDirection = dir, + Limit = info.PageSize + }; + if (info.Position != null) + args.RelativeUserId = info.Position.Value; + + var models = await client.ApiClient.GetGuildScheduledEventUsersAsync(guildEvent.Id, guildEvent.Guild.Id, args, options).ConfigureAwait(false); + var builder = ImmutableArray.CreateBuilder(); + foreach (var model in models) + { + builder.Add(RestUser.Create(client, guildEvent.Guild, model)); + } + return builder.ToImmutable(); + }, + nextPage: (info, lastPage) => + { + if (lastPage.Count != DiscordConfig.MaxGuildEventUsersPerBatch) + return false; + if (dir == Direction.Before) + info.Position = lastPage.Min(x => x.Id); + else + info.Position = lastPage.Max(x => x.Id); + return true; + }, + start: fromUserId, + count: limit + ); + } + + public static async Task ModifyGuildEventAsync(BaseDiscordClient client, Action func, + IGuildScheduledEvent guildEvent, RequestOptions options = null) + { + var args = new GuildScheduledEventsProperties(); + + func(args); + + if (args.Status.IsSpecified) + { + switch (args.Status.Value) + { + case GuildScheduledEventStatus.Active when guildEvent.Status != GuildScheduledEventStatus.Scheduled: + case GuildScheduledEventStatus.Completed when guildEvent.Status != GuildScheduledEventStatus.Active: + case GuildScheduledEventStatus.Cancelled when guildEvent.Status != GuildScheduledEventStatus.Scheduled: + throw new ArgumentException($"Cannot set event to {args.Status.Value} when events status is {guildEvent.Status}"); + } + } + + if (args.Type.IsSpecified) + { + // taken from https://discord.com/developers/docs/resources/guild-scheduled-event#modify-guild-scheduled-event + switch (args.Type.Value) + { + case GuildScheduledEventType.External: + if (!args.Location.IsSpecified) + throw new ArgumentException("Location must be specified for external events."); + if (!args.EndTime.IsSpecified) + throw new ArgumentException("End time must be specified for external events."); + if (!args.ChannelId.IsSpecified) + throw new ArgumentException("Channel id must be set to null!"); + if (args.ChannelId.Value != null) + throw new ArgumentException("Channel id must be set to null!"); + break; + } + } + + var apiArgs = new ModifyGuildScheduledEventParams() + { + ChannelId = args.ChannelId, + Description = args.Description, + EndTime = args.EndTime, + Name = args.Name, + PrivacyLevel = args.PrivacyLevel, + StartTime = args.StartTime, + Status = args.Status, + Type = args.Type + }; + + if(args.Location.IsSpecified) + { + apiArgs.EntityMetadata = new API.GuildScheduledEventEntityMetadata() + { + Location = args.Location, + }; + } + + return await client.ApiClient.ModifyGuildScheduledEventAsync(apiArgs, guildEvent.Id, guildEvent.Guild.Id, options).ConfigureAwait(false); + } + + public static async Task GetGuildEventAsync(BaseDiscordClient client, ulong id, IGuild guild, RequestOptions options = null) + { + var model = await client.ApiClient.GetGuildScheduledEventAsync(id, guild.Id, options).ConfigureAwait(false); + + if (model == null) + return null; + + return RestGuildEvent.Create(client, guild, model); + } + + public static async Task> GetGuildEventsAsync(BaseDiscordClient client, IGuild guild, RequestOptions options = null) + { + var models = await client.ApiClient.ListGuildScheduledEventsAsync(guild.Id, options).ConfigureAwait(false); + + return models.Select(x => RestGuildEvent.Create(client, guild, x)).ToImmutableArray(); + } + + public static async Task CreateGuildEventAsync(BaseDiscordClient client, IGuild guild, + string name, + GuildScheduledEventPrivacyLevel privacyLevel, + DateTimeOffset startTime, + GuildScheduledEventType type, + string description = null, + DateTimeOffset? endTime = null, + ulong? channelId = null, + string location = null, + RequestOptions options = null) + { + if(location != null) + { + Preconditions.AtMost(location.Length, 100, nameof(location)); + } + + switch (type) + { + case GuildScheduledEventType.Stage or GuildScheduledEventType.Voice when channelId == null: + throw new ArgumentException($"{nameof(channelId)} must not be null when type is {type}", nameof(channelId)); + case GuildScheduledEventType.External when channelId != null: + throw new ArgumentException($"{nameof(channelId)} must be null when using external event type", nameof(channelId)); + case GuildScheduledEventType.External when location == null: + throw new ArgumentException($"{nameof(location)} must not be null when using external event type", nameof(location)); + case GuildScheduledEventType.External when endTime == null: + throw new ArgumentException($"{nameof(endTime)} must not be null when using external event type", nameof(endTime)); + } + + if (startTime <= DateTimeOffset.Now) + throw new ArgumentOutOfRangeException(nameof(startTime), "The start time for an event cannot be in the past"); + + if (endTime != null && endTime <= startTime) + throw new ArgumentOutOfRangeException(nameof(endTime), $"{nameof(endTime)} cannot be before the start time"); + + var apiArgs = new CreateGuildScheduledEventParams() + { + ChannelId = channelId ?? Optional.Unspecified, + Description = description ?? Optional.Unspecified, + EndTime = endTime ?? Optional.Unspecified, + Name = name, + PrivacyLevel = privacyLevel, + StartTime = startTime, + Type = type + }; + + if(location != null) + { + apiArgs.EntityMetadata = new API.GuildScheduledEventEntityMetadata() + { + Location = location + }; + } + + var model = await client.ApiClient.CreateGuildScheduledEventAsync(apiArgs, guild.Id, options).ConfigureAwait(false); + + return RestGuildEvent.Create(client, guild, client.CurrentUser, model); + } + + public static async Task DeleteEventAsync(BaseDiscordClient client, IGuildScheduledEvent guildEvent, RequestOptions options = null) + { + await client.ApiClient.DeleteGuildScheduledEventAsync(guildEvent.Id, guildEvent.Guild.Id, options).ConfigureAwait(false); + } + + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 26dc8f3b7..9b0b66633 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -1109,6 +1109,65 @@ namespace Discord.Rest => sticker.DeleteAsync(options); #endregion + #region Guild Events + + /// + /// Gets an event within this guild. + /// + /// The snowflake identifier for the event. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. + /// + public Task GetEventAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetGuildEventAsync(Discord, id, this, options); + + /// + /// Gets all active events within this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. + /// + public Task> GetEventsAsync(RequestOptions options = null) + => GuildHelper.GetGuildEventsAsync(Discord, this, options); + + /// + /// Creates an event within this guild. + /// + /// The name of the event. + /// The privacy level of the event. + /// The start time of the event. + /// The type of the event. + /// The description of the event. + /// The end time of the event. + /// + /// The channel id of the event. + /// + /// The event must have a type of or + /// in order to use this property. + /// + /// + /// A collection of speakers for the event. + /// The location of the event; links are supported + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous create operation. + /// + public Task CreateEventAsync( + string name, + DateTimeOffset startTime, + GuildScheduledEventType type, + GuildScheduledEventPrivacyLevel privacyLevel = GuildScheduledEventPrivacyLevel.Private, + string description = null, + DateTimeOffset? endTime = null, + ulong? channelId = null, + string location = null, + RequestOptions options = null) + => GuildHelper.CreateGuildEventAsync(Discord, this, name, privacyLevel, startTime, type, description, endTime, channelId, location, options); + + #endregion + #region IGuild /// bool IGuild.Available => Available; @@ -1121,6 +1180,18 @@ namespace Discord.Rest IReadOnlyCollection IGuild.Stickers => Stickers; + /// + async Task IGuild.CreateEventAsync(string name, DateTimeOffset startTime, GuildScheduledEventType type, GuildScheduledEventPrivacyLevel privacyLevel, string description, DateTimeOffset? endTime, ulong? channelId, string location, RequestOptions options) + => await CreateEventAsync(name, startTime, type, privacyLevel, description, endTime, channelId, location, options).ConfigureAwait(false); + + /// + async Task IGuild.GetEventAsync(ulong id, RequestOptions options) + => await GetEventAsync(id, options).ConfigureAwait(false); + + /// + async Task> IGuild.GetEventsAsync(RequestOptions options) + => await GetEventsAsync(options).ConfigureAwait(false); + /// async Task> IGuild.GetBansAsync(RequestOptions options) => await GetBansAsync(options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuildEvent.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuildEvent.cs new file mode 100644 index 000000000..d3ec11fc6 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuildEvent.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.GuildScheduledEvent; + +namespace Discord.Rest +{ + public class RestGuildEvent : RestEntity, IGuildScheduledEvent + { + /// + public IGuild Guild { get; private set; } + + /// + public ulong? ChannelId { get; private set; } + + /// + public IUser Creator { get; private set; } + + /// + public ulong CreatorId { get; private set; } + + /// + public string Name { get; private set; } + + /// + public string Description { get; private set; } + + /// + public DateTimeOffset StartTime { get; private set; } + + /// + public DateTimeOffset? EndTime { get; private set; } + + /// + public GuildScheduledEventPrivacyLevel PrivacyLevel { get; private set; } + + /// + public GuildScheduledEventStatus Status { get; private set; } + + /// + public GuildScheduledEventType Type { get; private set; } + + /// + public ulong? EntityId { get; private set; } + + /// + public string Location { get; private set; } + + /// + public int? UserCount { get; private set; } + + internal RestGuildEvent(BaseDiscordClient client, IGuild guild, ulong id) + : base(client, id) + { + Guild = guild; + } + + internal static RestGuildEvent Create(BaseDiscordClient client, IGuild guild, Model model) + { + var entity = new RestGuildEvent(client, guild, model.Id); + entity.Update(model); + return entity; + } + + internal static RestGuildEvent Create(BaseDiscordClient client, IGuild guild, IUser creator, Model model) + { + var entity = new RestGuildEvent(client, guild, model.Id); + entity.Update(model, creator); + return entity; + } + + internal void Update(Model model, IUser creator) + { + Update(model); + Creator = creator; + CreatorId = creator.Id; + } + + internal void Update(Model model) + { + if (model.Creator.IsSpecified) + { + Creator = RestUser.Create(Discord, model.Creator.Value); + } + + CreatorId = model.CreatorId.ToNullable() ?? 0; // should be changed? + ChannelId = model.ChannelId.IsSpecified ? model.ChannelId.Value : null; + Name = model.Name; + Description = model.Description.GetValueOrDefault(); + StartTime = model.ScheduledStartTime; + EndTime = model.ScheduledEndTime; + PrivacyLevel = model.PrivacyLevel; + Status = model.Status; + Type = model.EntityType; + EntityId = model.EntityId; + Location = model.EntityMetadata?.Location.GetValueOrDefault(); + UserCount = model.UserCount.ToNullable(); + } + + /// + public Task StartAsync(RequestOptions options = null) + => ModifyAsync(x => x.Status = GuildScheduledEventStatus.Active); + + /// + public Task EndAsync(RequestOptions options = null) + => ModifyAsync(x => x.Status = Status == GuildScheduledEventStatus.Scheduled + ? GuildScheduledEventStatus.Cancelled + : GuildScheduledEventStatus.Completed); + + /// + public Task DeleteAsync(RequestOptions options = null) + => GuildHelper.DeleteEventAsync(Discord, this, options); + + /// + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await GuildHelper.ModifyGuildEventAsync(Discord, func, this, options).ConfigureAwait(false); + Update(model); + } + + /// + /// Gets a collection of N users interested in the event. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual messages as a + /// collection. + /// + /// This method will attempt to fetch all users that are interested in the event. + /// The library will attempt to split up the requests according to and . + /// In other words, if there are 300 users, and the constant + /// is 100, the request will be split into 3 individual requests; thus returning 3 individual asynchronous + /// responses, hence the need of flattening. + /// + /// The options to be used when sending the request. + /// + /// Paged collection of users. + /// + public IAsyncEnumerable> GetUsersAsync(RequestOptions options = null) + => GuildHelper.GetEventUsersAsync(Discord, this, null, null, options); + + /// + /// Gets a collection of N users interested in the event. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual users as a + /// collection. + /// + /// + /// Do not fetch too many users at once! This may cause unwanted preemptive rate limit or even actual + /// rate limit, causing your bot to freeze! + /// + /// This method will attempt to fetch the number of users specified under around + /// the user depending on the . The library will + /// attempt to split up the requests according to your and + /// . In other words, should the user request 500 users, + /// and the constant is 100, the request will + /// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need + /// of flattening. + /// + /// The ID of the starting user to get the users from. + /// The direction of the users to be gotten from. + /// The numbers of users to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of users. + /// + public IAsyncEnumerable> GetUsersAsync(ulong fromUserId, Direction dir, int limit = DiscordConfig.MaxGuildEventUsersPerBatch, RequestOptions options = null) + => GuildHelper.GetEventUsersAsync(Discord, this, fromUserId, dir, limit, options); + + #region IGuildScheduledEvent + + /// + IAsyncEnumerable> IGuildScheduledEvent.GetUsersAsync(RequestOptions options) + => GetUsersAsync(options); + /// + IAsyncEnumerable> IGuildScheduledEvent.GetUsersAsync(ulong fromUserId, Direction dir, int limit, RequestOptions options) + => GetUsersAsync(fromUserId, dir, limit, options); + + #endregion + } +} diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index 7304f5f39..872bab392 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Globalization; using System.Threading.Tasks; using Model = Discord.API.User; +using EventUserModel = Discord.API.GuildScheduledEventUser; namespace Discord.Rest { @@ -62,6 +63,18 @@ namespace Discord.Rest entity.Update(model); return entity; } + internal static RestUser Create(BaseDiscordClient discord, IGuild guild, EventUserModel model) + { + if (model.Member.IsSpecified) + { + var member = model.Member.Value; + member.User = model.User; + return RestGuildUser.Create(discord, guild, member); + } + else + return RestUser.Create(discord, model.User); + } + internal virtual void Update(Model model) { if (model.Avatar.IsSpecified) diff --git a/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs b/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs index 0b50cb166..876254fb9 100644 --- a/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs +++ b/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs @@ -27,7 +27,7 @@ namespace Discord.Net.Converters public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { - throw new NotImplementedException(); + writer.WriteValue(((DateTimeOffset)value).ToString("O")); } } } diff --git a/src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs b/src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs index 8470c6d8f..04ee38c0b 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs @@ -28,5 +28,8 @@ namespace Discord.API.Gateway [JsonProperty("threads")] public new Channel[] Threads { get; set; } + + [JsonProperty("guild_scheduled_events")] + public GuildScheduledEvent[] GuildScheduledEvents { get; set; } } } diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs index 531afad0e..153b320fa 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -344,6 +344,61 @@ namespace Discord.WebSocket internal readonly AsyncEvent, SocketGuild, Task>> _guildJoinRequestDeletedEvent = new AsyncEvent, SocketGuild, Task>>(); #endregion + #region Guild Events + + /// + /// Fired when a guild event is created. + /// + public event Func GuildScheduledEventCreated + { + add { _guildScheduledEventCreated.Add(value); } + remove { _guildScheduledEventCreated.Remove(value); } + } + internal readonly AsyncEvent> _guildScheduledEventCreated = new AsyncEvent>(); + + /// + /// Fired when a guild event is updated. + /// + public event Func, SocketGuildEvent, Task> GuildScheduledEventUpdated + { + add { _guildScheduledEventUpdated.Add(value); } + remove { _guildScheduledEventUpdated.Remove(value); } + } + internal readonly AsyncEvent, SocketGuildEvent, Task>> _guildScheduledEventUpdated = new AsyncEvent, SocketGuildEvent, Task>>(); + + + /// + /// Fired when a guild event is cancelled. + /// + public event Func GuildScheduledEventCancelled + { + add { _guildScheduledEventCancelled.Add(value); } + remove { _guildScheduledEventCancelled.Remove(value); } + } + internal readonly AsyncEvent> _guildScheduledEventCancelled = new AsyncEvent>(); + + /// + /// Fired when a guild event is completed. + /// + public event Func GuildScheduledEventCompleted + { + add { _guildScheduledEventCompleted.Add(value); } + remove { _guildScheduledEventCompleted.Remove(value); } + } + internal readonly AsyncEvent> _guildScheduledEventCompleted = new AsyncEvent>(); + + /// + /// Fired when a guild event is started. + /// + public event Func GuildScheduledEventStarted + { + add { _guildScheduledEventStarted.Add(value); } + remove { _guildScheduledEventStarted.Remove(value); } + } + internal readonly AsyncEvent> _guildScheduledEventStarted = new AsyncEvent>(); + + #endregion + #region Users /// Fired when a user joins a guild. public event Func UserJoined diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 39bcbbd81..cc651cd4d 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -486,6 +486,12 @@ namespace Discord.WebSocket client.GuildStickerDeleted += (sticker) => _guildStickerDeleted.InvokeAsync(sticker); client.GuildStickerUpdated += (before, after) => _guildStickerUpdated.InvokeAsync(before, after); client.GuildJoinRequestDeleted += (userId, guildId) => _guildJoinRequestDeletedEvent.InvokeAsync(userId, guildId); + + client.GuildScheduledEventCancelled += (arg) => _guildScheduledEventCancelled.InvokeAsync(arg); + client.GuildScheduledEventCompleted += (arg) => _guildScheduledEventCompleted.InvokeAsync(arg); + client.GuildScheduledEventCreated += (arg) => _guildScheduledEventCreated.InvokeAsync(arg); + client.GuildScheduledEventUpdated += (arg1, arg2) => _guildScheduledEventUpdated.InvokeAsync(arg1, arg2); + client.GuildScheduledEventStarted += (arg) => _guildScheduledEventStarted.InvokeAsync(arg); } #endregion diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 7460a3aa7..f34106a20 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -2549,19 +2549,91 @@ namespace Discord.WebSocket switch (type) { case "STAGE_INSTANCE_CREATE": - await TimedInvokeAsync(_stageStarted, nameof(StageStarted), stageChannel); + await TimedInvokeAsync(_stageStarted, nameof(StageStarted), stageChannel).ConfigureAwait(false); return; case "STAGE_INSTANCE_DELETE": - await TimedInvokeAsync(_stageEnded, nameof(StageEnded), stageChannel); + await TimedInvokeAsync(_stageEnded, nameof(StageEnded), stageChannel).ConfigureAwait(false); return; case "STAGE_INSTANCE_UPDATE": - await TimedInvokeAsync(_stageUpdated, nameof(StageUpdated), before, stageChannel); + await TimedInvokeAsync(_stageUpdated, nameof(StageUpdated), before, stageChannel).ConfigureAwait(false); return; } } break; #endregion + #region Guild Scheduled Events + case "GUILD_SCHEDULED_EVENT_CREATE": + { + await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + + var newEvent = guild.AddOrUpdateEvent(data); + + await TimedInvokeAsync(_guildScheduledEventCancelled, nameof(GuildScheduledEventCreated), newEvent).ConfigureAwait(false); + } + break; + case "GUILD_SCHEDULED_EVENT_UPDATE": + { + await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + + var before = guild.GetEvent(data.Id); + var beforeCacheable = new Cacheable(before, data.Id, before != null, () => Task.FromResult((SocketGuildEvent)null)); + + var after = guild.AddOrUpdateEvent(data); + + if((before != null ? before.Status != GuildScheduledEventStatus.Completed : true) && data.Status == GuildScheduledEventStatus.Completed) + { + await TimedInvokeAsync(_guildScheduledEventCompleted, nameof(GuildScheduledEventCompleted), after).ConfigureAwait(false); + } + else if((before != null ? before.Status != GuildScheduledEventStatus.Active : false) && data.Status == GuildScheduledEventStatus.Active) + { + await TimedInvokeAsync(_guildScheduledEventStarted, nameof(GuildScheduledEventStarted), after).ConfigureAwait(false); + } + else await TimedInvokeAsync(_guildScheduledEventUpdated, nameof(GuildScheduledEventUpdated), beforeCacheable, after).ConfigureAwait(false); + } + break; + case "GUILD_SCHEDULED_EVENT_DELETE": + { + await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + + var guildEvent = guild.RemoveEvent(data.Id) ?? SocketGuildEvent.Create(this, guild, data); + + await TimedInvokeAsync(_guildScheduledEventCancelled, nameof(GuildScheduledEventCancelled), guildEvent).ConfigureAwait(false); + } + break; + + #endregion + #region Ignored (User only) case "CHANNEL_PINS_ACK": await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_ACK)").ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index e190f9b23..beaab1cfe 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -20,6 +20,7 @@ using RoleModel = Discord.API.Role; using UserModel = Discord.API.User; using VoiceStateModel = Discord.API.VoiceState; using StickerModel = Discord.API.Sticker; +using EventModel = Discord.API.GuildScheduledEvent; using System.IO; namespace Discord.WebSocket @@ -40,6 +41,7 @@ namespace Discord.WebSocket private ConcurrentDictionary _roles; private ConcurrentDictionary _voiceStates; private ConcurrentDictionary _stickers; + private ConcurrentDictionary _events; private ImmutableArray _emotes; private AudioClient _audioClient; @@ -364,6 +366,17 @@ namespace Discord.WebSocket /// public IReadOnlyCollection Roles => _roles.ToReadOnlyCollection(); + /// + /// Gets a collection of all events within this guild. + /// + /// + /// This field is based off of caching alone, since there is no events returned on the guild model. + /// + /// + /// A read-only collection of guild events found within this guild. + /// + public IReadOnlyCollection Events => _events.ToReadOnlyCollection(); + internal SocketGuild(DiscordSocketClient client, ulong id) : base(client, id) { @@ -381,6 +394,8 @@ namespace Discord.WebSocket IsAvailable = !(model.Unavailable ?? false); if (!IsAvailable) { + if(_events == null) + _events = new ConcurrentDictionary(); if (_channels == null) _channels = new ConcurrentDictionary(); if (_members == null) @@ -449,7 +464,16 @@ namespace Discord.WebSocket } _voiceStates = voiceStates; - + var events = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.GuildScheduledEvents.Length * 1.05)); + { + for (int i = 0; i < model.GuildScheduledEvents.Length; i++) + { + var guildEvent = SocketGuildEvent.Create(Discord, this, model.GuildScheduledEvents[i]); + events.TryAdd(guildEvent.Id, guildEvent); + } + } + _events = events; + _syncPromise = new TaskCompletionSource(); _downloaderPromise = new TaskCompletionSource(); @@ -1191,6 +1215,115 @@ namespace Discord.WebSocket => GuildHelper.SearchUsersAsync(this, Discord, query, limit, options); #endregion + #region Guild Events + + /// + /// Gets an event in this guild. + /// + /// The snowflake identifier for the event. + /// + /// An event that is associated with the specified ; if none is found. + /// + public SocketGuildEvent GetEvent(ulong id) + { + if (_events.TryGetValue(id, out SocketGuildEvent value)) + return value; + return null; + } + + internal SocketGuildEvent RemoveEvent(ulong id) + { + if (_events.TryRemove(id, out SocketGuildEvent value)) + return value; + return null; + } + + internal SocketGuildEvent AddOrUpdateEvent(EventModel model) + { + if (_events.TryGetValue(model.Id, out SocketGuildEvent value)) + value.Update(model); + else + { + value = SocketGuildEvent.Create(Discord, this, model); + _events[model.Id] = value; + } + return value; + } + + /// + /// Gets an event within this guild. + /// + /// The snowflake identifier for the event. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. + /// + public Task GetEventAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetGuildEventAsync(Discord, id, this, options); + + /// + /// Gets all active events within this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. + /// + public Task> GetEventsAsync(RequestOptions options = null) + => GuildHelper.GetGuildEventsAsync(Discord, this, options); + + /// + /// Creates an event within this guild. + /// + /// The name of the event. + /// The privacy level of the event. + /// The start time of the event. + /// The type of the event. + /// The description of the event. + /// The end time of the event. + /// + /// The channel id of the event. + /// + /// The event must have a type of or + /// in order to use this property. + /// + /// + /// A collection of speakers for the event. + /// The location of the event; links are supported + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous create operation. + /// + public Task CreateEventAsync( + string name, + DateTimeOffset startTime, + GuildScheduledEventType type, + GuildScheduledEventPrivacyLevel privacyLevel = GuildScheduledEventPrivacyLevel.Private, + string description = null, + DateTimeOffset? endTime = null, + ulong? channelId = null, + string location = null, + RequestOptions options = null) + { + // requirements taken from https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-permissions-requirements + switch (type) + { + case GuildScheduledEventType.Stage: + CurrentUser.GuildPermissions.Ensure(GuildPermission.ManageEvents | GuildPermission.ManageChannels | GuildPermission.MuteMembers | GuildPermission.MoveMembers); + break; + case GuildScheduledEventType.Voice: + CurrentUser.GuildPermissions.Ensure(GuildPermission.ManageEvents | GuildPermission.ViewChannel | GuildPermission.Connect); + break; + case GuildScheduledEventType.External: + CurrentUser.GuildPermissions.Ensure(GuildPermission.ManageEvents); + break; + } + + return GuildHelper.CreateGuildEventAsync(Discord, this, name, privacyLevel, startTime, type, description, endTime, channelId, location, options); + } + + + #endregion + #region Audit logs /// /// Gets the specified number of audit log entries for this guild. @@ -1625,6 +1758,15 @@ namespace Discord.WebSocket /// IReadOnlyCollection IGuild.Stickers => Stickers; /// + async Task IGuild.CreateEventAsync(string name, DateTimeOffset startTime, GuildScheduledEventType type, GuildScheduledEventPrivacyLevel privacyLevel, string description, DateTimeOffset? endTime, ulong? channelId, string location, RequestOptions options) + => await CreateEventAsync(name, startTime, type, privacyLevel, description, endTime, channelId, location, options).ConfigureAwait(false); + /// + async Task IGuild.GetEventAsync(ulong id, RequestOptions options) + => await GetEventAsync(id, options).ConfigureAwait(false); + /// + async Task> IGuild.GetEventsAsync(RequestOptions options) + => await GetEventsAsync(options).ConfigureAwait(false); + /// async Task> IGuild.GetBansAsync(RequestOptions options) => await GetBansAsync(options).ConfigureAwait(false); /// diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs new file mode 100644 index 000000000..6974c0498 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs @@ -0,0 +1,216 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.GuildScheduledEvent; + +namespace Discord.WebSocket +{ + /// + /// Represents a WebSocket-based guild event. + /// + public class SocketGuildEvent : SocketEntity, IGuildScheduledEvent + { + /// + /// Gets the guild of the event. + /// + public SocketGuild Guild { get; private set; } + + /// + /// Gets the channel of the event. + /// + public SocketGuildChannel Channel { get; private set; } + + /// + /// Gets the user who created the event. + /// + public SocketGuildUser Creator { get; private set; } + + /// + public string Name { get; private set; } + + /// + public string Description { get; private set; } + + /// + public DateTimeOffset StartTime { get; private set; } + + /// + public DateTimeOffset? EndTime { get; private set; } + + /// + public GuildScheduledEventPrivacyLevel PrivacyLevel { get; private set; } + + /// + public GuildScheduledEventStatus Status { get; private set; } + + /// + public GuildScheduledEventType Type { get; private set; } + + /// + public ulong? EntityId { get; private set; } + + /// + public string Location { get; private set; } + + /// + public int? UserCount { get; private set; } + + internal SocketGuildEvent(DiscordSocketClient client, SocketGuild guild, ulong id) + : base(client, id) + { + Guild = guild; + } + + internal static SocketGuildEvent Create(DiscordSocketClient client, SocketGuild guild, Model model) + { + var entity = new SocketGuildEvent(client, guild, model.Id); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + if (model.ChannelId.IsSpecified && model.ChannelId.Value != null) + { + Channel = Guild.GetChannel(model.ChannelId.Value.Value); + } + + if (model.CreatorId.IsSpecified) + { + var guildUser = Guild.GetUser(model.CreatorId.Value); + + if(guildUser != null) + { + if(model.Creator.IsSpecified) + guildUser.Update(Discord.State, model.Creator.Value); + + Creator = guildUser; + } + else if (guildUser == null && model.Creator.IsSpecified) + { + guildUser = SocketGuildUser.Create(Guild, Discord.State, model.Creator.Value); + Creator = guildUser; + } + } + + Name = model.Name; + Description = model.Description.GetValueOrDefault(); + + EntityId = model.EntityId; + Location = model.EntityMetadata?.Location.GetValueOrDefault(); + Type = model.EntityType; + + PrivacyLevel = model.PrivacyLevel; + EndTime = model.ScheduledEndTime; + StartTime = model.ScheduledStartTime; + Status = model.Status; + UserCount = model.UserCount.ToNullable(); + } + + /// + public Task DeleteAsync(RequestOptions options = null) + => GuildHelper.DeleteEventAsync(Discord, this, options); + + /// + public Task StartAsync(RequestOptions options = null) + => ModifyAsync(x => x.Status = GuildScheduledEventStatus.Active); + + /// + public Task EndAsync(RequestOptions options = null) + => ModifyAsync(x => x.Status = Status == GuildScheduledEventStatus.Scheduled + ? GuildScheduledEventStatus.Cancelled + : GuildScheduledEventStatus.Completed); + + /// + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await GuildHelper.ModifyGuildEventAsync(Discord, func, this, options).ConfigureAwait(false); + Update(model); + } + + /// + /// Gets a collection of users that are interested in this event. + /// + /// The amount of users to fetch. + /// The options to be used when sending the request. + /// + /// A read-only collection of users. + /// + public Task> GetUsersAsync(int limit = 100, RequestOptions options = null) + => GuildHelper.GetEventUsersAsync(Discord, this, limit, options); + + /// + /// Gets a collection of N users interested in the event. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual messages as a + /// collection. + /// + /// This method will attempt to fetch all users that are interested in the event. + /// The library will attempt to split up the requests according to and . + /// In other words, if there are 300 users, and the constant + /// is 100, the request will be split into 3 individual requests; thus returning 3 individual asynchronous + /// responses, hence the need of flattening. + /// + /// The options to be used when sending the request. + /// + /// Paged collection of users. + /// + public IAsyncEnumerable> GetUsersAsync(RequestOptions options = null) + => GuildHelper.GetEventUsersAsync(Discord, this, null, null, options); + + /// + /// Gets a collection of N users interested in the event. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual users as a + /// collection. + /// + /// + /// Do not fetch too many users at once! This may cause unwanted preemptive rate limit or even actual + /// rate limit, causing your bot to freeze! + /// + /// This method will attempt to fetch the number of users specified under around + /// the user depending on the . The library will + /// attempt to split up the requests according to your and + /// . In other words, should the user request 500 users, + /// and the constant is 100, the request will + /// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need + /// of flattening. + /// + /// The ID of the starting user to get the users from. + /// The direction of the users to be gotten from. + /// The numbers of users to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of users. + /// + public IAsyncEnumerable> GetUsersAsync(ulong fromUserId, Direction dir, int limit = DiscordConfig.MaxGuildEventUsersPerBatch, RequestOptions options = null) + => GuildHelper.GetEventUsersAsync(Discord, this, fromUserId, dir, limit, options); + + #region IGuildScheduledEvent + + /// + IAsyncEnumerable> IGuildScheduledEvent.GetUsersAsync(RequestOptions options) + => GetUsersAsync(options); + /// + IAsyncEnumerable> IGuildScheduledEvent.GetUsersAsync(ulong fromUserId, Direction dir, int limit, RequestOptions options) + => GetUsersAsync(fromUserId, dir, limit, options); + /// + IGuild IGuildScheduledEvent.Guild => Guild; + /// + IUser IGuildScheduledEvent.Creator => Creator; + /// + ulong? IGuildScheduledEvent.ChannelId => Channel?.Id; + + #endregion + } +} diff --git a/test/Discord.Net.Tests.Unit/GuildPermissionsTests.cs b/test/Discord.Net.Tests.Unit/GuildPermissionsTests.cs index 00ca05504..f0b0b2db7 100644 --- a/test/Discord.Net.Tests.Unit/GuildPermissionsTests.cs +++ b/test/Discord.Net.Tests.Unit/GuildPermissionsTests.cs @@ -94,6 +94,7 @@ namespace Discord AssertFlag(() => new GuildPermissions(manageEmojisAndStickers: true), GuildPermission.ManageEmojisAndStickers); AssertFlag(() => new GuildPermissions(useApplicationCommands: true), GuildPermission.UseApplicationCommands); AssertFlag(() => new GuildPermissions(requestToSpeak: true), GuildPermission.RequestToSpeak); + AssertFlag(() => new GuildPermissions(manageEvents: true), GuildPermission.ManageEvents); AssertFlag(() => new GuildPermissions(manageThreads: true), GuildPermission.ManageThreads); AssertFlag(() => new GuildPermissions(createPublicThreads: true), GuildPermission.CreatePublicThreads); AssertFlag(() => new GuildPermissions(createPrivateThreads: true), GuildPermission.CreatePrivateThreads); @@ -170,6 +171,7 @@ namespace Discord AssertUtil(GuildPermission.ManageEmojisAndStickers, x => x.ManageEmojisAndStickers, (p, enable) => p.Modify(manageEmojisAndStickers: enable)); AssertUtil(GuildPermission.UseApplicationCommands, x => x.UseApplicationCommands, (p, enable) => p.Modify(useApplicationCommands: enable)); AssertUtil(GuildPermission.RequestToSpeak, x => x.RequestToSpeak, (p, enable) => p.Modify(requestToSpeak: enable)); + AssertUtil(GuildPermission.ManageEvents, x => x.ManageEvents, (p, enable) => p.Modify(manageEvents: enable)); AssertUtil(GuildPermission.ManageThreads, x => x.ManageThreads, (p, enable) => p.Modify(manageThreads: enable)); AssertUtil(GuildPermission.CreatePublicThreads, x => x.CreatePublicThreads, (p, enable) => p.Modify(createPublicThreads: enable)); AssertUtil(GuildPermission.CreatePrivateThreads, x => x.CreatePrivateThreads, (p, enable) => p.Modify(createPrivateThreads: enable));