* guild events initial * sharded events * Add new gateway intents and fix bugs * More work on new changes to guild events * Update guild scheduled events * Added events to extended guild and add event start event * Update preconditions * Implement breaking changes guild guild events. Add guild event permissions * Update tests and change privacy level requirements * Update summaries and add docs for guild eventspull/1923/head
| @@ -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. | |||
| @@ -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(); | |||
| ``` | |||
| @@ -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 += ... | |||
| ``` | |||
| @@ -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. | |||
| @@ -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 | |||
| @@ -94,6 +94,13 @@ namespace Discord | |||
| /// The maximum number of users that can be gotten per-batch. | |||
| /// </returns> | |||
| public const int MaxUsersPerBatch = 1000; | |||
| /// <summary> | |||
| /// Returns the max users allowed to be in a request for guild event users. | |||
| /// </summary> | |||
| /// <returns> | |||
| /// The maximum number of users that can be gotten per-batch. | |||
| /// </returns> | |||
| public const int MaxGuildEventUsersPerBatch = 100; | |||
| /// <summary> | |||
| /// Returns the max guilds allowed to be in a request. | |||
| /// </summary> | |||
| @@ -0,0 +1,25 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| /// <summary> | |||
| /// Represents the privacy level of a guild scheduled event. | |||
| /// </summary> | |||
| public enum GuildScheduledEventPrivacyLevel | |||
| { | |||
| /// <summary> | |||
| /// The scheduled event is public and available in discovery. | |||
| /// </summary> | |||
| [Obsolete("This event type isn't supported yet! check back later.", true)] | |||
| Public = 1, | |||
| /// <summary> | |||
| /// The scheduled event is only accessible to guild members. | |||
| /// </summary> | |||
| Private = 2, | |||
| } | |||
| } | |||
| @@ -0,0 +1,34 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| /// <summary> | |||
| /// Represents the status of a guild event. | |||
| /// </summary> | |||
| public enum GuildScheduledEventStatus | |||
| { | |||
| /// <summary> | |||
| /// The event is scheduled for a set time. | |||
| /// </summary> | |||
| Scheduled = 1, | |||
| /// <summary> | |||
| /// The event has started. | |||
| /// </summary> | |||
| Active = 2, | |||
| /// <summary> | |||
| /// The event was completed. | |||
| /// </summary> | |||
| Completed = 3, | |||
| /// <summary> | |||
| /// The event was canceled. | |||
| /// </summary> | |||
| Cancelled = 4, | |||
| } | |||
| } | |||
| @@ -0,0 +1,34 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| /// <summary> | |||
| /// Represents the type of a guild scheduled event. | |||
| /// </summary> | |||
| public enum GuildScheduledEventType | |||
| { | |||
| /// <summary> | |||
| /// The event doesn't have a set type. | |||
| /// </summary> | |||
| None = 0, | |||
| /// <summary> | |||
| /// The event is set in a stage channel. | |||
| /// </summary> | |||
| Stage = 1, | |||
| /// <summary> | |||
| /// The event is set in a voice channel. | |||
| /// </summary> | |||
| Voice = 2, | |||
| /// <summary> | |||
| /// The event is set for somewhere externally from discord. | |||
| /// </summary> | |||
| External = 3, | |||
| } | |||
| } | |||
| @@ -0,0 +1,58 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| /// <summary> | |||
| /// Provides properties that are used to modify an <see cref="IGuildScheduledEvent" /> with the specified changes. | |||
| /// </summary> | |||
| public class GuildScheduledEventsProperties | |||
| { | |||
| /// <summary> | |||
| /// Gets or sets the channel id of the event. | |||
| /// </summary> | |||
| public Optional<ulong?> ChannelId { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets the location of this event. | |||
| /// </summary> | |||
| public Optional<string> Location { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets the name of the event. | |||
| /// </summary> | |||
| public Optional<string> Name { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets the privacy level of the event. | |||
| /// </summary> | |||
| public Optional<GuildScheduledEventPrivacyLevel> PrivacyLevel { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets the start time of the event. | |||
| /// </summary> | |||
| public Optional<DateTimeOffset> StartTime { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets the end time of the event. | |||
| /// </summary> | |||
| public Optional<DateTimeOffset> EndTime { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets the description of the event. | |||
| /// </summary> | |||
| public Optional<string> Description { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets the type of the event. | |||
| /// </summary> | |||
| public Optional<GuildScheduledEventType> Type { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets the status of the event. | |||
| /// </summary> | |||
| public Optional<GuildScheduledEventStatus> Status { get; set; } | |||
| } | |||
| } | |||
| @@ -1056,6 +1056,58 @@ namespace Discord | |||
| /// </returns> | |||
| Task DeleteStickerAsync(ICustomSticker sticker, RequestOptions options = null); | |||
| /// <summary> | |||
| /// Gets a event within this guild. | |||
| /// </summary> | |||
| /// <param name="id">The id of the event.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous get operation. | |||
| /// </returns> | |||
| Task<IGuildScheduledEvent> GetEventAsync(ulong id, RequestOptions options = null); | |||
| /// <summary> | |||
| /// Gets a collection of events within this guild. | |||
| /// </summary> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous get operation. | |||
| /// </returns> | |||
| Task<IReadOnlyCollection<IGuildScheduledEvent>> GetEventsAsync(RequestOptions options = null); | |||
| /// <summary> | |||
| /// Creates an event within this guild. | |||
| /// </summary> | |||
| /// <param name="name">The name of the event.</param> | |||
| /// <param name="privacyLevel">The privacy level of the event.</param> | |||
| /// <param name="startTime">The start time of the event.</param> | |||
| /// <param name="type">The type of the event.</param> | |||
| /// <param name="description">The description of the event.</param> | |||
| /// <param name="endTime">The end time of the event.</param> | |||
| /// <param name="channelId"> | |||
| /// The channel id of the event. | |||
| /// <remarks> | |||
| /// The event must have a type of <see cref="GuildScheduledEventType.Stage"/> or <see cref="GuildScheduledEventType.Voice"/> | |||
| /// in order to use this property. | |||
| /// </remarks> | |||
| /// </param> | |||
| /// <param name="speakers">A collection of speakers for the event.</param> | |||
| /// <param name="location">The location of the event; links are supported</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous create operation. | |||
| /// </returns> | |||
| Task<IGuildScheduledEvent> 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); | |||
| /// <summary> | |||
| /// Gets this guilds application commands. | |||
| /// </summary> | |||
| @@ -0,0 +1,170 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| /// <summary> | |||
| /// Represents a generic guild scheduled event. | |||
| /// </summary> | |||
| public interface IGuildScheduledEvent : IEntity<ulong> | |||
| { | |||
| /// <summary> | |||
| /// Gets the guild this event is scheduled in. | |||
| /// </summary> | |||
| IGuild Guild { get; } | |||
| /// <summary> | |||
| /// Gets the optional channel id where this event will be hosted. | |||
| /// </summary> | |||
| ulong? ChannelId { get; } | |||
| /// <summary> | |||
| /// Gets the user who created the event. | |||
| /// </summary> | |||
| IUser Creator { get; } | |||
| /// <summary> | |||
| /// Gets the name of the event. | |||
| /// </summary> | |||
| string Name { get; } | |||
| /// <summary> | |||
| /// Gets the description of the event. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// This field is <see langword="null"/> when the event doesn't have a discription. | |||
| /// </remarks> | |||
| string Description { get; } | |||
| /// <summary> | |||
| /// Gets the start time of the event. | |||
| /// </summary> | |||
| DateTimeOffset StartTime { get; } | |||
| /// <summary> | |||
| /// Gets the optional end time of the event. | |||
| /// </summary> | |||
| DateTimeOffset? EndTime { get; } | |||
| /// <summary> | |||
| /// Gets the privacy level of the event. | |||
| /// </summary> | |||
| GuildScheduledEventPrivacyLevel PrivacyLevel { get; } | |||
| /// <summary> | |||
| /// Gets the status of the event. | |||
| /// </summary> | |||
| GuildScheduledEventStatus Status { get; } | |||
| /// <summary> | |||
| /// Gets the type of the event. | |||
| /// </summary> | |||
| GuildScheduledEventType Type { get; } | |||
| /// <summary> | |||
| /// Gets the optional entity id of the event. The "entity" of the event | |||
| /// can be a stage instance event as is seperate from <see cref="ChannelId"/>. | |||
| /// </summary> | |||
| ulong? EntityId { get; } | |||
| /// <summary> | |||
| /// Gets the location of the event if the <see cref="Type"/> is external. | |||
| /// </summary> | |||
| string Location { get; } | |||
| /// <summary> | |||
| /// Gets the user count of the event. | |||
| /// </summary> | |||
| int? UserCount { get; } | |||
| /// <summary> | |||
| /// Starts the event. | |||
| /// </summary> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous start operation. | |||
| /// </returns> | |||
| Task StartAsync(RequestOptions options = null); | |||
| /// <summary> | |||
| /// Ends or canceles the event. | |||
| /// </summary> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous end operation. | |||
| /// </returns> | |||
| Task EndAsync(RequestOptions options = null); | |||
| /// <summary> | |||
| /// Modifies the guild event. | |||
| /// </summary> | |||
| /// <param name="func">The delegate containing the properties to modify the event with.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous modification operation. | |||
| /// </returns> | |||
| Task ModifyAsync(Action<GuildScheduledEventsProperties> func, RequestOptions options = null); | |||
| /// <summary> | |||
| /// Deletes the current event. | |||
| /// </summary> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous delete operation. | |||
| /// </returns> | |||
| Task DeleteAsync(RequestOptions options = null); | |||
| /// <summary> | |||
| /// Gets a collection of N users interested in the event. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// <note type="important"> | |||
| /// The returned collection is an asynchronous enumerable object; one must call | |||
| /// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> to access the individual messages as a | |||
| /// collection. | |||
| /// </note> | |||
| /// 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 <see cref="DiscordConfig.MaxGuildEventUsersPerBatch"/>. | |||
| /// In other words, if there are 300 users, and the <see cref="Discord.DiscordConfig.MaxGuildEventUsersPerBatch"/> constant | |||
| /// is <c>100</c>, the request will be split into 3 individual requests; thus returning 3 individual asynchronous | |||
| /// responses, hence the need of flattening. | |||
| /// </remarks> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// Paged collection of users. | |||
| /// </returns> | |||
| IAsyncEnumerable<IReadOnlyCollection<IUser>> GetUsersAsync(RequestOptions options = null); | |||
| /// <summary> | |||
| /// Gets a collection of N users interested in the event. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// <note type="important"> | |||
| /// The returned collection is an asynchronous enumerable object; one must call | |||
| /// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> to access the individual users as a | |||
| /// collection. | |||
| /// </note> | |||
| /// <note type="warning"> | |||
| /// 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! | |||
| /// </note> | |||
| /// This method will attempt to fetch the number of users specified under <paramref name="limit"/> around | |||
| /// the user <paramref name="fromUserId"/> depending on the <paramref name="dir"/>. The library will | |||
| /// attempt to split up the requests according to your <paramref name="limit"/> and | |||
| /// <see cref="DiscordConfig.MaxGuildEventUsersPerBatch"/>. In other words, should the user request 500 users, | |||
| /// and the <see cref="Discord.DiscordConfig.MaxGuildEventUsersPerBatch"/> constant is <c>100</c>, the request will | |||
| /// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need | |||
| /// of flattening. | |||
| /// </remarks> | |||
| /// <param name="fromUserId">The ID of the starting user to get the users from.</param> | |||
| /// <param name="dir">The direction of the users to be gotten from.</param> | |||
| /// <param name="limit">The numbers of users to be gotten from.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// Paged collection of users. | |||
| /// </returns> | |||
| IAsyncEnumerable<IReadOnlyCollection<IUser>> GetUsersAsync(ulong fromUserId, Direction dir, int limit = DiscordConfig.MaxGuildEventUsersPerBatch, RequestOptions options = null); | |||
| } | |||
| } | |||
| @@ -185,10 +185,14 @@ namespace Discord | |||
| /// </summary> | |||
| UseApplicationCommands = 0x80_00_00_00, | |||
| /// <summary> | |||
| /// Allows for requesting to speak in stage channels. <i>(This permission is under active development and may be changed or removed.)</i>. | |||
| /// Allows for requesting to speak in stage channels. | |||
| /// </summary> | |||
| RequestToSpeak = 0x01_00_00_00_00, | |||
| /// <summary> | |||
| /// Allows for creating, editing, and deleting guild scheduled events. | |||
| /// </summary> | |||
| ManageEvents = 0x02_00_00_00_00, | |||
| /// <summary> | |||
| /// Allows for deleting and archiving threads, and viewing all private threads. | |||
| /// </summary> | |||
| /// <remarks> | |||
| @@ -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); | |||
| /// <summary> If <c>true</c>, a user may request to speak in stage channels. </summary> | |||
| public bool RequestToSpeak => Permissions.GetValue(RawValue, GuildPermission.RequestToSpeak); | |||
| /// <summary> If <c>true</c>, a user may create, edit, and delete events. </summary> | |||
| public bool ManageEvents => Permissions.GetValue(RawValue, GuildPermission.ManageEvents); | |||
| /// <summary> If <c>true</c>, a user may manage threads in this guild. </summary> | |||
| public bool ManageThreads => Permissions.GetValue(RawValue, GuildPermission.ManageThreads); | |||
| /// <summary> If <c>true</c>, a user may create public threads in this guild. </summary> | |||
| @@ -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); | |||
| /// <summary> | |||
| @@ -351,6 +359,18 @@ namespace Discord | |||
| return perms; | |||
| } | |||
| internal void Ensure(GuildPermission permissions) | |||
| { | |||
| if (!Has(permissions)) | |||
| { | |||
| var vals = Enum.GetValues(typeof(GuildPermission)).Cast<GuildPermission>(); | |||
| 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())}"; | |||
| } | |||
| @@ -39,13 +39,15 @@ namespace Discord | |||
| DirectMessageReactions = 1 << 13, | |||
| /// <summary> This intent includes TYPING_START </summary> | |||
| DirectMessageTyping = 1 << 14, | |||
| /// <summary> 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 </summary> | |||
| GuildScheduledEvents = 1 << 16, | |||
| /// <summary> | |||
| /// This intent includes all but <see cref="GuildMembers"/> and <see cref="GuildPresences"/> | |||
| /// which are privileged and must be enabled in the Developer Portal. | |||
| /// </summary> | |||
| AllUnprivileged = Guilds | GuildBans | GuildEmojis | GuildIntegrations | GuildWebhooks | GuildInvites | | |||
| GuildVoiceStates | GuildMessages | GuildMessageReactions | GuildMessageTyping | DirectMessages | | |||
| DirectMessageReactions | DirectMessageTyping, | |||
| DirectMessageReactions | DirectMessageTyping | GuildScheduledEvents, | |||
| /// <summary> | |||
| /// This intent includes all of them, including privileged ones. | |||
| /// </summary> | |||
| @@ -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<ulong?> ChannelId { get; set; } | |||
| [JsonProperty("creator_id")] | |||
| public Optional<ulong> CreatorId { get; set; } | |||
| [JsonProperty("name")] | |||
| public string Name { get; set; } | |||
| [JsonProperty("description")] | |||
| public Optional<string> 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<User> Creator { get; set; } | |||
| [JsonProperty("user_count")] | |||
| public Optional<int> UserCount { get; set; } | |||
| } | |||
| } | |||
| @@ -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<string> Location { get; set; } | |||
| } | |||
| } | |||
| @@ -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<GuildMember> Member { get; set; } | |||
| [JsonProperty("guild_scheduled_event_id")] | |||
| public ulong GuildScheduledEventId { get; set; } | |||
| } | |||
| } | |||
| @@ -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<ulong> ChannelId { get; set; } | |||
| [JsonProperty("entity_metadata")] | |||
| public Optional<GuildScheduledEventEntityMetadata> 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<DateTimeOffset> EndTime { get; set; } | |||
| [JsonProperty("description")] | |||
| public Optional<string> Description { get; set; } | |||
| [JsonProperty("entity_type")] | |||
| public GuildScheduledEventType Type { get; set; } | |||
| } | |||
| } | |||
| @@ -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<int> Limit { get; set; } | |||
| public Optional<Direction> RelativeDirection { get; set; } | |||
| public Optional<ulong> RelativeUserId { get; set; } | |||
| } | |||
| } | |||
| @@ -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<ulong?> ChannelId { get; set; } | |||
| [JsonProperty("entity_metadata")] | |||
| public Optional<GuildScheduledEventEntityMetadata> EntityMetadata { get; set; } | |||
| [JsonProperty("name")] | |||
| public Optional<string> Name { get; set; } | |||
| [JsonProperty("privacy_level")] | |||
| public Optional<GuildScheduledEventPrivacyLevel> PrivacyLevel { get; set; } | |||
| [JsonProperty("scheduled_start_time")] | |||
| public Optional<DateTimeOffset> StartTime { get; set; } | |||
| [JsonProperty("scheduled_end_time")] | |||
| public Optional<DateTimeOffset> EndTime { get; set; } | |||
| [JsonProperty("description")] | |||
| public Optional<string> Description { get; set; } | |||
| [JsonProperty("entity_type")] | |||
| public Optional<GuildScheduledEventType> Type { get; set; } | |||
| [JsonProperty("status")] | |||
| public Optional<GuildScheduledEventStatus> Status { get; set; } | |||
| } | |||
| } | |||
| @@ -1909,6 +1909,105 @@ namespace Discord.API | |||
| } | |||
| #endregion | |||
| #region Guild Events | |||
| public async Task<GuildScheduledEvent[]> 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<GuildScheduledEvent[]>("GET", () => $"guilds/{guildId}/scheduled-events?with_user_count=true", ids, options: options).ConfigureAwait(false); | |||
| } | |||
| public async Task<GuildScheduledEvent> 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<GuildScheduledEvent>(SendAsync<GuildScheduledEvent>("GET", () => $"guilds/{guildId}/scheduled-events/{eventId}?with_user_count=true", ids, options: options)).ConfigureAwait(false); | |||
| } | |||
| public async Task<GuildScheduledEvent> 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<GuildScheduledEvent>("POST", () => $"guilds/{guildId}/scheduled-events", args, ids, options: options).ConfigureAwait(false); | |||
| } | |||
| public async Task<GuildScheduledEvent> 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<GuildScheduledEvent>("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<GuildScheduledEventUser[]> 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<GuildScheduledEventUser[]>("GET", () => $"guilds/{guildId}/scheduled-events/{eventId}/users?limit={limit}&with_member=true", ids, options: options).ConfigureAwait(false); | |||
| } | |||
| public async Task<GuildScheduledEventUser[]> 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<Func<string>> 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<GuildScheduledEventUser[]>("GET", endpoint, ids, options: options).ConfigureAwait(false); | |||
| } | |||
| #endregion | |||
| #region Users | |||
| public async Task<User> GetUserAsync(ulong userId, RequestOptions options = null) | |||
| { | |||
| @@ -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<IReadOnlyCollection<RestUser>> 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<IReadOnlyCollection<RestUser>> GetEventUsersAsync(BaseDiscordClient client, IGuildScheduledEvent guildEvent, | |||
| ulong? fromUserId, int? limit, RequestOptions options) | |||
| { | |||
| return new PagedAsyncEnumerable<RestUser>( | |||
| 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<IReadOnlyCollection<RestUser>> 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<RestUser>( | |||
| 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<RestUser>(); | |||
| 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<API.GuildScheduledEvent> ModifyGuildEventAsync(BaseDiscordClient client, Action<GuildScheduledEventsProperties> 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<RestGuildEvent> 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<IReadOnlyCollection<RestGuildEvent>> 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<RestGuildEvent> 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<ulong>.Unspecified, | |||
| Description = description ?? Optional<string>.Unspecified, | |||
| EndTime = endTime ?? Optional<DateTimeOffset>.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 | |||
| } | |||
| } | |||
| @@ -1109,6 +1109,65 @@ namespace Discord.Rest | |||
| => sticker.DeleteAsync(options); | |||
| #endregion | |||
| #region Guild Events | |||
| /// <summary> | |||
| /// Gets an event within this guild. | |||
| /// </summary> | |||
| /// <param name="id">The snowflake identifier for the event.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous get operation. | |||
| /// </returns> | |||
| public Task<RestGuildEvent> GetEventAsync(ulong id, RequestOptions options = null) | |||
| => GuildHelper.GetGuildEventAsync(Discord, id, this, options); | |||
| /// <summary> | |||
| /// Gets all active events within this guild. | |||
| /// </summary> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous get operation. | |||
| /// </returns> | |||
| public Task<IReadOnlyCollection<RestGuildEvent>> GetEventsAsync(RequestOptions options = null) | |||
| => GuildHelper.GetGuildEventsAsync(Discord, this, options); | |||
| /// <summary> | |||
| /// Creates an event within this guild. | |||
| /// </summary> | |||
| /// <param name="name">The name of the event.</param> | |||
| /// <param name="privacyLevel">The privacy level of the event.</param> | |||
| /// <param name="startTime">The start time of the event.</param> | |||
| /// <param name="type">The type of the event.</param> | |||
| /// <param name="description">The description of the event.</param> | |||
| /// <param name="endTime">The end time of the event.</param> | |||
| /// <param name="channelId"> | |||
| /// The channel id of the event. | |||
| /// <remarks> | |||
| /// The event must have a type of <see cref="GuildScheduledEventType.Stage"/> or <see cref="GuildScheduledEventType.Voice"/> | |||
| /// in order to use this property. | |||
| /// </remarks> | |||
| /// </param> | |||
| /// <param name="speakers">A collection of speakers for the event.</param> | |||
| /// <param name="location">The location of the event; links are supported</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous create operation. | |||
| /// </returns> | |||
| public Task<RestGuildEvent> 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 | |||
| /// <inheritdoc /> | |||
| bool IGuild.Available => Available; | |||
| @@ -1121,6 +1180,18 @@ namespace Discord.Rest | |||
| IReadOnlyCollection<ICustomSticker> IGuild.Stickers => Stickers; | |||
| /// <inheritdoc /> | |||
| async Task<IGuildScheduledEvent> 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); | |||
| /// <inheritdoc /> | |||
| async Task<IGuildScheduledEvent> IGuild.GetEventAsync(ulong id, RequestOptions options) | |||
| => await GetEventAsync(id, options).ConfigureAwait(false); | |||
| /// <inheritdoc /> | |||
| async Task<IReadOnlyCollection<IGuildScheduledEvent>> IGuild.GetEventsAsync(RequestOptions options) | |||
| => await GetEventsAsync(options).ConfigureAwait(false); | |||
| /// <inheritdoc /> | |||
| async Task<IReadOnlyCollection<IBan>> IGuild.GetBansAsync(RequestOptions options) | |||
| => await GetBansAsync(options).ConfigureAwait(false); | |||
| @@ -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<ulong>, IGuildScheduledEvent | |||
| { | |||
| /// <inheritdoc/> | |||
| public IGuild Guild { get; private set; } | |||
| /// <inheritdoc/> | |||
| public ulong? ChannelId { get; private set; } | |||
| /// <inheritdoc/> | |||
| public IUser Creator { get; private set; } | |||
| /// <inheritdoc/> | |||
| public ulong CreatorId { get; private set; } | |||
| /// <inheritdoc/> | |||
| public string Name { get; private set; } | |||
| /// <inheritdoc/> | |||
| public string Description { get; private set; } | |||
| /// <inheritdoc/> | |||
| public DateTimeOffset StartTime { get; private set; } | |||
| /// <inheritdoc/> | |||
| public DateTimeOffset? EndTime { get; private set; } | |||
| /// <inheritdoc/> | |||
| public GuildScheduledEventPrivacyLevel PrivacyLevel { get; private set; } | |||
| /// <inheritdoc/> | |||
| public GuildScheduledEventStatus Status { get; private set; } | |||
| /// <inheritdoc/> | |||
| public GuildScheduledEventType Type { get; private set; } | |||
| /// <inheritdoc/> | |||
| public ulong? EntityId { get; private set; } | |||
| /// <inheritdoc/> | |||
| public string Location { get; private set; } | |||
| /// <inheritdoc/> | |||
| 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(); | |||
| } | |||
| /// <inheritdoc/> | |||
| public Task StartAsync(RequestOptions options = null) | |||
| => ModifyAsync(x => x.Status = GuildScheduledEventStatus.Active); | |||
| /// <inheritdoc/> | |||
| public Task EndAsync(RequestOptions options = null) | |||
| => ModifyAsync(x => x.Status = Status == GuildScheduledEventStatus.Scheduled | |||
| ? GuildScheduledEventStatus.Cancelled | |||
| : GuildScheduledEventStatus.Completed); | |||
| /// <inheritdoc/> | |||
| public Task DeleteAsync(RequestOptions options = null) | |||
| => GuildHelper.DeleteEventAsync(Discord, this, options); | |||
| /// <inheritdoc/> | |||
| public async Task ModifyAsync(Action<GuildScheduledEventsProperties> func, RequestOptions options = null) | |||
| { | |||
| var model = await GuildHelper.ModifyGuildEventAsync(Discord, func, this, options).ConfigureAwait(false); | |||
| Update(model); | |||
| } | |||
| /// <summary> | |||
| /// Gets a collection of N users interested in the event. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// <note type="important"> | |||
| /// The returned collection is an asynchronous enumerable object; one must call | |||
| /// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> to access the individual messages as a | |||
| /// collection. | |||
| /// </note> | |||
| /// 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 <see cref="DiscordConfig.MaxGuildEventUsersPerBatch"/>. | |||
| /// In other words, if there are 300 users, and the <see cref="Discord.DiscordConfig.MaxGuildEventUsersPerBatch"/> constant | |||
| /// is <c>100</c>, the request will be split into 3 individual requests; thus returning 3 individual asynchronous | |||
| /// responses, hence the need of flattening. | |||
| /// </remarks> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// Paged collection of users. | |||
| /// </returns> | |||
| public IAsyncEnumerable<IReadOnlyCollection<RestUser>> GetUsersAsync(RequestOptions options = null) | |||
| => GuildHelper.GetEventUsersAsync(Discord, this, null, null, options); | |||
| /// <summary> | |||
| /// Gets a collection of N users interested in the event. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// <note type="important"> | |||
| /// The returned collection is an asynchronous enumerable object; one must call | |||
| /// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> to access the individual users as a | |||
| /// collection. | |||
| /// </note> | |||
| /// <note type="warning"> | |||
| /// 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! | |||
| /// </note> | |||
| /// This method will attempt to fetch the number of users specified under <paramref name="limit"/> around | |||
| /// the user <paramref name="fromUserId"/> depending on the <paramref name="dir"/>. The library will | |||
| /// attempt to split up the requests according to your <paramref name="limit"/> and | |||
| /// <see cref="DiscordConfig.MaxGuildEventUsersPerBatch"/>. In other words, should the user request 500 users, | |||
| /// and the <see cref="Discord.DiscordConfig.MaxGuildEventUsersPerBatch"/> constant is <c>100</c>, the request will | |||
| /// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need | |||
| /// of flattening. | |||
| /// </remarks> | |||
| /// <param name="fromUserId">The ID of the starting user to get the users from.</param> | |||
| /// <param name="dir">The direction of the users to be gotten from.</param> | |||
| /// <param name="limit">The numbers of users to be gotten from.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// Paged collection of users. | |||
| /// </returns> | |||
| public IAsyncEnumerable<IReadOnlyCollection<RestUser>> GetUsersAsync(ulong fromUserId, Direction dir, int limit = DiscordConfig.MaxGuildEventUsersPerBatch, RequestOptions options = null) | |||
| => GuildHelper.GetEventUsersAsync(Discord, this, fromUserId, dir, limit, options); | |||
| #region IGuildScheduledEvent | |||
| /// <inheritdoc/> | |||
| IAsyncEnumerable<IReadOnlyCollection<IUser>> IGuildScheduledEvent.GetUsersAsync(RequestOptions options) | |||
| => GetUsersAsync(options); | |||
| /// <inheritdoc/> | |||
| IAsyncEnumerable<IReadOnlyCollection<IUser>> IGuildScheduledEvent.GetUsersAsync(ulong fromUserId, Direction dir, int limit, RequestOptions options) | |||
| => GetUsersAsync(fromUserId, dir, limit, options); | |||
| #endregion | |||
| } | |||
| } | |||
| @@ -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) | |||
| @@ -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")); | |||
| } | |||
| } | |||
| } | |||
| @@ -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; } | |||
| } | |||
| } | |||
| @@ -344,6 +344,61 @@ namespace Discord.WebSocket | |||
| internal readonly AsyncEvent<Func<Cacheable<SocketGuildUser, ulong>, SocketGuild, Task>> _guildJoinRequestDeletedEvent = new AsyncEvent<Func<Cacheable<SocketGuildUser, ulong>, SocketGuild, Task>>(); | |||
| #endregion | |||
| #region Guild Events | |||
| /// <summary> | |||
| /// Fired when a guild event is created. | |||
| /// </summary> | |||
| public event Func<SocketGuildEvent, Task> GuildScheduledEventCreated | |||
| { | |||
| add { _guildScheduledEventCreated.Add(value); } | |||
| remove { _guildScheduledEventCreated.Remove(value); } | |||
| } | |||
| internal readonly AsyncEvent<Func<SocketGuildEvent, Task>> _guildScheduledEventCreated = new AsyncEvent<Func<SocketGuildEvent, Task>>(); | |||
| /// <summary> | |||
| /// Fired when a guild event is updated. | |||
| /// </summary> | |||
| public event Func<Cacheable<SocketGuildEvent, ulong>, SocketGuildEvent, Task> GuildScheduledEventUpdated | |||
| { | |||
| add { _guildScheduledEventUpdated.Add(value); } | |||
| remove { _guildScheduledEventUpdated.Remove(value); } | |||
| } | |||
| internal readonly AsyncEvent<Func<Cacheable<SocketGuildEvent, ulong>, SocketGuildEvent, Task>> _guildScheduledEventUpdated = new AsyncEvent<Func<Cacheable<SocketGuildEvent, ulong>, SocketGuildEvent, Task>>(); | |||
| /// <summary> | |||
| /// Fired when a guild event is cancelled. | |||
| /// </summary> | |||
| public event Func<SocketGuildEvent, Task> GuildScheduledEventCancelled | |||
| { | |||
| add { _guildScheduledEventCancelled.Add(value); } | |||
| remove { _guildScheduledEventCancelled.Remove(value); } | |||
| } | |||
| internal readonly AsyncEvent<Func<SocketGuildEvent, Task>> _guildScheduledEventCancelled = new AsyncEvent<Func<SocketGuildEvent, Task>>(); | |||
| /// <summary> | |||
| /// Fired when a guild event is completed. | |||
| /// </summary> | |||
| public event Func<SocketGuildEvent, Task> GuildScheduledEventCompleted | |||
| { | |||
| add { _guildScheduledEventCompleted.Add(value); } | |||
| remove { _guildScheduledEventCompleted.Remove(value); } | |||
| } | |||
| internal readonly AsyncEvent<Func<SocketGuildEvent, Task>> _guildScheduledEventCompleted = new AsyncEvent<Func<SocketGuildEvent, Task>>(); | |||
| /// <summary> | |||
| /// Fired when a guild event is started. | |||
| /// </summary> | |||
| public event Func<SocketGuildEvent, Task> GuildScheduledEventStarted | |||
| { | |||
| add { _guildScheduledEventStarted.Add(value); } | |||
| remove { _guildScheduledEventStarted.Remove(value); } | |||
| } | |||
| internal readonly AsyncEvent<Func<SocketGuildEvent, Task>> _guildScheduledEventStarted = new AsyncEvent<Func<SocketGuildEvent, Task>>(); | |||
| #endregion | |||
| #region Users | |||
| /// <summary> Fired when a user joins a guild. </summary> | |||
| public event Func<SocketGuildUser, Task> UserJoined | |||
| @@ -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 | |||
| @@ -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<GuildScheduledEvent>(_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<GuildScheduledEvent>(_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<SocketGuildEvent, ulong>(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<GuildScheduledEvent>(_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); | |||
| @@ -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<ulong, SocketRole> _roles; | |||
| private ConcurrentDictionary<ulong, SocketVoiceState> _voiceStates; | |||
| private ConcurrentDictionary<ulong, SocketCustomSticker> _stickers; | |||
| private ConcurrentDictionary<ulong, SocketGuildEvent> _events; | |||
| private ImmutableArray<GuildEmote> _emotes; | |||
| private AudioClient _audioClient; | |||
| @@ -364,6 +366,17 @@ namespace Discord.WebSocket | |||
| /// </returns> | |||
| public IReadOnlyCollection<SocketRole> Roles => _roles.ToReadOnlyCollection(); | |||
| /// <summary> | |||
| /// Gets a collection of all events within this guild. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// This field is based off of caching alone, since there is no events returned on the guild model. | |||
| /// </remarks> | |||
| /// <returns> | |||
| /// A read-only collection of guild events found within this guild. | |||
| /// </returns> | |||
| public IReadOnlyCollection<SocketGuildEvent> 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<ulong, SocketGuildEvent>(); | |||
| if (_channels == null) | |||
| _channels = new ConcurrentDictionary<ulong, SocketGuildChannel>(); | |||
| if (_members == null) | |||
| @@ -449,7 +464,16 @@ namespace Discord.WebSocket | |||
| } | |||
| _voiceStates = voiceStates; | |||
| var events = new ConcurrentDictionary<ulong, SocketGuildEvent>(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<bool>(); | |||
| _downloaderPromise = new TaskCompletionSource<bool>(); | |||
| @@ -1191,6 +1215,115 @@ namespace Discord.WebSocket | |||
| => GuildHelper.SearchUsersAsync(this, Discord, query, limit, options); | |||
| #endregion | |||
| #region Guild Events | |||
| /// <summary> | |||
| /// Gets an event in this guild. | |||
| /// </summary> | |||
| /// <param name="id">The snowflake identifier for the event.</param> | |||
| /// <returns> | |||
| /// An event that is associated with the specified <paramref name="id"/>; <see langword="null"/> if none is found. | |||
| /// </returns> | |||
| 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; | |||
| } | |||
| /// <summary> | |||
| /// Gets an event within this guild. | |||
| /// </summary> | |||
| /// <param name="id">The snowflake identifier for the event.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous get operation. | |||
| /// </returns> | |||
| public Task<RestGuildEvent> GetEventAsync(ulong id, RequestOptions options = null) | |||
| => GuildHelper.GetGuildEventAsync(Discord, id, this, options); | |||
| /// <summary> | |||
| /// Gets all active events within this guild. | |||
| /// </summary> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous get operation. | |||
| /// </returns> | |||
| public Task<IReadOnlyCollection<RestGuildEvent>> GetEventsAsync(RequestOptions options = null) | |||
| => GuildHelper.GetGuildEventsAsync(Discord, this, options); | |||
| /// <summary> | |||
| /// Creates an event within this guild. | |||
| /// </summary> | |||
| /// <param name="name">The name of the event.</param> | |||
| /// <param name="privacyLevel">The privacy level of the event.</param> | |||
| /// <param name="startTime">The start time of the event.</param> | |||
| /// <param name="type">The type of the event.</param> | |||
| /// <param name="description">The description of the event.</param> | |||
| /// <param name="endTime">The end time of the event.</param> | |||
| /// <param name="channelId"> | |||
| /// The channel id of the event. | |||
| /// <remarks> | |||
| /// The event must have a type of <see cref="GuildScheduledEventType.Stage"/> or <see cref="GuildScheduledEventType.Voice"/> | |||
| /// in order to use this property. | |||
| /// </remarks> | |||
| /// </param> | |||
| /// <param name="speakers">A collection of speakers for the event.</param> | |||
| /// <param name="location">The location of the event; links are supported</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous create operation. | |||
| /// </returns> | |||
| public Task<RestGuildEvent> 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 | |||
| /// <summary> | |||
| /// Gets the specified number of audit log entries for this guild. | |||
| @@ -1625,6 +1758,15 @@ namespace Discord.WebSocket | |||
| /// <inheritdoc /> | |||
| IReadOnlyCollection<ICustomSticker> IGuild.Stickers => Stickers; | |||
| /// <inheritdoc /> | |||
| async Task<IGuildScheduledEvent> 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); | |||
| /// <inheritdoc /> | |||
| async Task<IGuildScheduledEvent> IGuild.GetEventAsync(ulong id, RequestOptions options) | |||
| => await GetEventAsync(id, options).ConfigureAwait(false); | |||
| /// <inheritdoc /> | |||
| async Task<IReadOnlyCollection<IGuildScheduledEvent>> IGuild.GetEventsAsync(RequestOptions options) | |||
| => await GetEventsAsync(options).ConfigureAwait(false); | |||
| /// <inheritdoc /> | |||
| async Task<IReadOnlyCollection<IBan>> IGuild.GetBansAsync(RequestOptions options) | |||
| => await GetBansAsync(options).ConfigureAwait(false); | |||
| /// <inheritdoc/> | |||
| @@ -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 | |||
| { | |||
| /// <summary> | |||
| /// Represents a WebSocket-based guild event. | |||
| /// </summary> | |||
| public class SocketGuildEvent : SocketEntity<ulong>, IGuildScheduledEvent | |||
| { | |||
| /// <summary> | |||
| /// Gets the guild of the event. | |||
| /// </summary> | |||
| public SocketGuild Guild { get; private set; } | |||
| /// <summary> | |||
| /// Gets the channel of the event. | |||
| /// </summary> | |||
| public SocketGuildChannel Channel { get; private set; } | |||
| /// <summary> | |||
| /// Gets the user who created the event. | |||
| /// </summary> | |||
| public SocketGuildUser Creator { get; private set; } | |||
| /// <inheritdoc/> | |||
| public string Name { get; private set; } | |||
| /// <inheritdoc/> | |||
| public string Description { get; private set; } | |||
| /// <inheritdoc/> | |||
| public DateTimeOffset StartTime { get; private set; } | |||
| /// <inheritdoc/> | |||
| public DateTimeOffset? EndTime { get; private set; } | |||
| /// <inheritdoc/> | |||
| public GuildScheduledEventPrivacyLevel PrivacyLevel { get; private set; } | |||
| /// <inheritdoc/> | |||
| public GuildScheduledEventStatus Status { get; private set; } | |||
| /// <inheritdoc/> | |||
| public GuildScheduledEventType Type { get; private set; } | |||
| /// <inheritdoc/> | |||
| public ulong? EntityId { get; private set; } | |||
| /// <inheritdoc/> | |||
| public string Location { get; private set; } | |||
| /// <inheritdoc/> | |||
| 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(); | |||
| } | |||
| /// <inheritdoc/> | |||
| public Task DeleteAsync(RequestOptions options = null) | |||
| => GuildHelper.DeleteEventAsync(Discord, this, options); | |||
| /// <inheritdoc/> | |||
| public Task StartAsync(RequestOptions options = null) | |||
| => ModifyAsync(x => x.Status = GuildScheduledEventStatus.Active); | |||
| /// <inheritdoc/> | |||
| public Task EndAsync(RequestOptions options = null) | |||
| => ModifyAsync(x => x.Status = Status == GuildScheduledEventStatus.Scheduled | |||
| ? GuildScheduledEventStatus.Cancelled | |||
| : GuildScheduledEventStatus.Completed); | |||
| /// <inheritdoc/> | |||
| public async Task ModifyAsync(Action<GuildScheduledEventsProperties> func, RequestOptions options = null) | |||
| { | |||
| var model = await GuildHelper.ModifyGuildEventAsync(Discord, func, this, options).ConfigureAwait(false); | |||
| Update(model); | |||
| } | |||
| /// <summary> | |||
| /// Gets a collection of users that are interested in this event. | |||
| /// </summary> | |||
| /// <param name="limit">The amount of users to fetch.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// A read-only collection of users. | |||
| /// </returns> | |||
| public Task<IReadOnlyCollection<RestUser>> GetUsersAsync(int limit = 100, RequestOptions options = null) | |||
| => GuildHelper.GetEventUsersAsync(Discord, this, limit, options); | |||
| /// <summary> | |||
| /// Gets a collection of N users interested in the event. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// <note type="important"> | |||
| /// The returned collection is an asynchronous enumerable object; one must call | |||
| /// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> to access the individual messages as a | |||
| /// collection. | |||
| /// </note> | |||
| /// 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 <see cref="DiscordConfig.MaxGuildEventUsersPerBatch"/>. | |||
| /// In other words, if there are 300 users, and the <see cref="Discord.DiscordConfig.MaxGuildEventUsersPerBatch"/> constant | |||
| /// is <c>100</c>, the request will be split into 3 individual requests; thus returning 3 individual asynchronous | |||
| /// responses, hence the need of flattening. | |||
| /// </remarks> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// Paged collection of users. | |||
| /// </returns> | |||
| public IAsyncEnumerable<IReadOnlyCollection<RestUser>> GetUsersAsync(RequestOptions options = null) | |||
| => GuildHelper.GetEventUsersAsync(Discord, this, null, null, options); | |||
| /// <summary> | |||
| /// Gets a collection of N users interested in the event. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// <note type="important"> | |||
| /// The returned collection is an asynchronous enumerable object; one must call | |||
| /// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> to access the individual users as a | |||
| /// collection. | |||
| /// </note> | |||
| /// <note type="warning"> | |||
| /// 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! | |||
| /// </note> | |||
| /// This method will attempt to fetch the number of users specified under <paramref name="limit"/> around | |||
| /// the user <paramref name="fromUserId"/> depending on the <paramref name="dir"/>. The library will | |||
| /// attempt to split up the requests according to your <paramref name="limit"/> and | |||
| /// <see cref="DiscordConfig.MaxGuildEventUsersPerBatch"/>. In other words, should the user request 500 users, | |||
| /// and the <see cref="Discord.DiscordConfig.MaxGuildEventUsersPerBatch"/> constant is <c>100</c>, the request will | |||
| /// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need | |||
| /// of flattening. | |||
| /// </remarks> | |||
| /// <param name="fromUserId">The ID of the starting user to get the users from.</param> | |||
| /// <param name="dir">The direction of the users to be gotten from.</param> | |||
| /// <param name="limit">The numbers of users to be gotten from.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// Paged collection of users. | |||
| /// </returns> | |||
| public IAsyncEnumerable<IReadOnlyCollection<RestUser>> GetUsersAsync(ulong fromUserId, Direction dir, int limit = DiscordConfig.MaxGuildEventUsersPerBatch, RequestOptions options = null) | |||
| => GuildHelper.GetEventUsersAsync(Discord, this, fromUserId, dir, limit, options); | |||
| #region IGuildScheduledEvent | |||
| /// <inheritdoc/> | |||
| IAsyncEnumerable<IReadOnlyCollection<IUser>> IGuildScheduledEvent.GetUsersAsync(RequestOptions options) | |||
| => GetUsersAsync(options); | |||
| /// <inheritdoc/> | |||
| IAsyncEnumerable<IReadOnlyCollection<IUser>> IGuildScheduledEvent.GetUsersAsync(ulong fromUserId, Direction dir, int limit, RequestOptions options) | |||
| => GetUsersAsync(fromUserId, dir, limit, options); | |||
| /// <inheritdoc/> | |||
| IGuild IGuildScheduledEvent.Guild => Guild; | |||
| /// <inheritdoc/> | |||
| IUser IGuildScheduledEvent.Creator => Creator; | |||
| /// <inheritdoc/> | |||
| ulong? IGuildScheduledEvent.ChannelId => Channel?.Id; | |||
| #endregion | |||
| } | |||
| } | |||
| @@ -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)); | |||