Browse Source

Add Guild Scheduled Events (#279)

* 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 events
pull/1923/head
Quin Lynch GitHub 3 years ago
parent
commit
e41e1a15c5
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1784 additions and 9 deletions
  1. +31
    -0
      docs/guides/guild_events/creating-guild-events.md
  2. +16
    -0
      docs/guides/guild_events/getting-event-users.md
  3. +41
    -0
      docs/guides/guild_events/intro.md
  4. +23
    -0
      docs/guides/guild_events/modifying-events.md
  5. +10
    -0
      docs/guides/toc.yml
  6. +7
    -0
      src/Discord.Net.Core/DiscordConfig.cs
  7. +25
    -0
      src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventPrivacyLevel.cs
  8. +34
    -0
      src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventStatus.cs
  9. +34
    -0
      src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventType.cs
  10. +58
    -0
      src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventsProperties.cs
  11. +52
    -0
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  12. +170
    -0
      src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs
  13. +5
    -1
      src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs
  14. +21
    -1
      src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs
  15. +3
    -1
      src/Discord.Net.Core/GatewayIntents.cs
  16. +43
    -0
      src/Discord.Net.Rest/API/Common/GuildScheduledEvent.cs
  17. +15
    -0
      src/Discord.Net.Rest/API/Common/GuildScheduledEventEntityMetadata.cs
  18. +19
    -0
      src/Discord.Net.Rest/API/Common/GuildScheduledEventUser.cs
  19. +29
    -0
      src/Discord.Net.Rest/API/Rest/CreateGuildScheduledEventParams.cs
  20. +15
    -0
      src/Discord.Net.Rest/API/Rest/GetEventUsersParams.cs
  21. +31
    -0
      src/Discord.Net.Rest/API/Rest/ModifyGuildScheduledEventParams.cs
  22. +99
    -0
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  23. +230
    -1
      src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
  24. +71
    -0
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  25. +188
    -0
      src/Discord.Net.Rest/Entities/Guilds/RestGuildEvent.cs
  26. +13
    -0
      src/Discord.Net.Rest/Entities/Users/RestUser.cs
  27. +1
    -1
      src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs
  28. +3
    -0
      src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs
  29. +55
    -0
      src/Discord.Net.WebSocket/BaseSocketClient.Events.cs
  30. +6
    -0
      src/Discord.Net.WebSocket/DiscordShardedClient.cs
  31. +75
    -3
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  32. +143
    -1
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  33. +216
    -0
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs
  34. +2
    -0
      test/Discord.Net.Tests.Unit/GuildPermissionsTests.cs

+ 31
- 0
docs/guides/guild_events/creating-guild-events.md View File

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

+ 16
- 0
docs/guides/guild_events/getting-event-users.md View File

@@ -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();
```

+ 41
- 0
docs/guides/guild_events/intro.md View File

@@ -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 += ...
```

+ 23
- 0
docs/guides/guild_events/modifying-events.md View File

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

+ 10
- 0
docs/guides/toc.yml View File

@@ -1,5 +1,15 @@
- name: Introduction - name: Introduction
topicUid: Guides.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 - name: Working with Slash commands
items: items:
- name: Introduction - name: Introduction


+ 7
- 0
src/Discord.Net.Core/DiscordConfig.cs View File

@@ -94,6 +94,13 @@ namespace Discord
/// The maximum number of users that can be gotten per-batch. /// The maximum number of users that can be gotten per-batch.
/// </returns> /// </returns>
public const int MaxUsersPerBatch = 1000; 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> /// <summary>
/// Returns the max guilds allowed to be in a request. /// Returns the max guilds allowed to be in a request.
/// </summary> /// </summary>


+ 25
- 0
src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventPrivacyLevel.cs View File

@@ -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,
}
}

+ 34
- 0
src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventStatus.cs View File

@@ -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,
}
}

+ 34
- 0
src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventType.cs View File

@@ -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,
}
}

+ 58
- 0
src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventsProperties.cs View File

@@ -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; }
}
}

+ 52
- 0
src/Discord.Net.Core/Entities/Guilds/IGuild.cs View File

@@ -1056,6 +1056,58 @@ namespace Discord
/// </returns> /// </returns>
Task DeleteStickerAsync(ICustomSticker sticker, RequestOptions options = null); 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> /// <summary>
/// Gets this guilds application commands. /// Gets this guilds application commands.
/// </summary> /// </summary>


+ 170
- 0
src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs View File

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

+ 5
- 1
src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs View File

@@ -185,10 +185,14 @@ namespace Discord
/// </summary> /// </summary>
UseApplicationCommands = 0x80_00_00_00, UseApplicationCommands = 0x80_00_00_00,
/// <summary> /// <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> /// </summary>
RequestToSpeak = 0x01_00_00_00_00, RequestToSpeak = 0x01_00_00_00_00,
/// <summary> /// <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. /// Allows for deleting and archiving threads, and viewing all private threads.
/// </summary> /// </summary>
/// <remarks> /// <remarks>


+ 21
- 1
src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;


namespace Discord namespace Discord
{ {
@@ -87,6 +88,8 @@ namespace Discord
public bool UseApplicationCommands => Permissions.GetValue(RawValue, GuildPermission.UseApplicationCommands); public bool UseApplicationCommands => Permissions.GetValue(RawValue, GuildPermission.UseApplicationCommands);
/// <summary> If <c>true</c>, a user may request to speak in stage channels. </summary> /// <summary> If <c>true</c>, a user may request to speak in stage channels. </summary>
public bool RequestToSpeak => Permissions.GetValue(RawValue, GuildPermission.RequestToSpeak); 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> /// <summary> If <c>true</c>, a user may manage threads in this guild. </summary>
public bool ManageThreads => Permissions.GetValue(RawValue, GuildPermission.ManageThreads); public bool ManageThreads => Permissions.GetValue(RawValue, GuildPermission.ManageThreads);
/// <summary> If <c>true</c>, a user may create public threads in this guild. </summary> /// <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? manageEmojisAndStickers = null,
bool? useApplicationCommands = null, bool? useApplicationCommands = null,
bool? requestToSpeak = null, bool? requestToSpeak = null,
bool? manageEvents = null,
bool? manageThreads = null, bool? manageThreads = null,
bool? createPublicThreads = null, bool? createPublicThreads = null,
bool? createPrivateThreads = null, bool? createPrivateThreads = null,
@@ -182,6 +186,7 @@ namespace Discord
Permissions.SetValue(ref value, manageEmojisAndStickers, GuildPermission.ManageEmojisAndStickers); Permissions.SetValue(ref value, manageEmojisAndStickers, GuildPermission.ManageEmojisAndStickers);
Permissions.SetValue(ref value, useApplicationCommands, GuildPermission.UseApplicationCommands); Permissions.SetValue(ref value, useApplicationCommands, GuildPermission.UseApplicationCommands);
Permissions.SetValue(ref value, requestToSpeak, GuildPermission.RequestToSpeak); 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, manageThreads, GuildPermission.ManageThreads);
Permissions.SetValue(ref value, createPublicThreads, GuildPermission.CreatePublicThreads); Permissions.SetValue(ref value, createPublicThreads, GuildPermission.CreatePublicThreads);
Permissions.SetValue(ref value, createPrivateThreads, GuildPermission.CreatePrivateThreads); Permissions.SetValue(ref value, createPrivateThreads, GuildPermission.CreatePrivateThreads);
@@ -227,6 +232,7 @@ namespace Discord
bool manageEmojisAndStickers = false, bool manageEmojisAndStickers = false,
bool useApplicationCommands = false, bool useApplicationCommands = false,
bool requestToSpeak = false, bool requestToSpeak = false,
bool manageEvents = false,
bool manageThreads = false, bool manageThreads = false,
bool createPublicThreads = false, bool createPublicThreads = false,
bool createPrivateThreads = false, bool createPrivateThreads = false,
@@ -267,6 +273,7 @@ namespace Discord
manageEmojisAndStickers: manageEmojisAndStickers, manageEmojisAndStickers: manageEmojisAndStickers,
useApplicationCommands: useApplicationCommands, useApplicationCommands: useApplicationCommands,
requestToSpeak: requestToSpeak, requestToSpeak: requestToSpeak,
manageEvents: manageEvents,
manageThreads: manageThreads, manageThreads: manageThreads,
createPublicThreads: createPublicThreads, createPublicThreads: createPublicThreads,
createPrivateThreads: createPrivateThreads, createPrivateThreads: createPrivateThreads,
@@ -310,6 +317,7 @@ namespace Discord
bool? manageEmojisAndStickers = null, bool? manageEmojisAndStickers = null,
bool? useApplicationCommands = null, bool? useApplicationCommands = null,
bool? requestToSpeak = null, bool? requestToSpeak = null,
bool? manageEvents = null,
bool? manageThreads = null, bool? manageThreads = null,
bool? createPublicThreads = null, bool? createPublicThreads = null,
bool? createPrivateThreads = null, bool? createPrivateThreads = null,
@@ -320,7 +328,7 @@ namespace Discord
viewAuditLog, viewGuildInsights, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, viewAuditLog, viewGuildInsights, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles,
readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, moveMembers, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, moveMembers,
useVoiceActivation, prioritySpeaker, stream, changeNickname, manageNicknames, manageRoles, manageWebhooks, manageEmojisAndStickers, useVoiceActivation, prioritySpeaker, stream, changeNickname, manageNicknames, manageRoles, manageWebhooks, manageEmojisAndStickers,
useApplicationCommands, requestToSpeak, manageThreads, createPublicThreads, createPrivateThreads, useExternalStickers, sendMessagesInThreads,
useApplicationCommands, requestToSpeak, manageEvents, manageThreads, createPublicThreads, createPrivateThreads, useExternalStickers, sendMessagesInThreads,
startEmbeddedActivities); startEmbeddedActivities);


/// <summary> /// <summary>
@@ -351,6 +359,18 @@ namespace Discord
return perms; 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(); public override string ToString() => RawValue.ToString();
private string DebuggerDisplay => $"{string.Join(", ", ToList())}"; private string DebuggerDisplay => $"{string.Join(", ", ToList())}";
} }


+ 3
- 1
src/Discord.Net.Core/GatewayIntents.cs View File

@@ -39,13 +39,15 @@ namespace Discord
DirectMessageReactions = 1 << 13, DirectMessageReactions = 1 << 13,
/// <summary> This intent includes TYPING_START </summary> /// <summary> This intent includes TYPING_START </summary>
DirectMessageTyping = 1 << 14, 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> /// <summary>
/// This intent includes all but <see cref="GuildMembers"/> and <see cref="GuildPresences"/> /// This intent includes all but <see cref="GuildMembers"/> and <see cref="GuildPresences"/>
/// which are privileged and must be enabled in the Developer Portal. /// which are privileged and must be enabled in the Developer Portal.
/// </summary> /// </summary>
AllUnprivileged = Guilds | GuildBans | GuildEmojis | GuildIntegrations | GuildWebhooks | GuildInvites | AllUnprivileged = Guilds | GuildBans | GuildEmojis | GuildIntegrations | GuildWebhooks | GuildInvites |
GuildVoiceStates | GuildMessages | GuildMessageReactions | GuildMessageTyping | DirectMessages | GuildVoiceStates | GuildMessages | GuildMessageReactions | GuildMessageTyping | DirectMessages |
DirectMessageReactions | DirectMessageTyping,
DirectMessageReactions | DirectMessageTyping | GuildScheduledEvents,
/// <summary> /// <summary>
/// This intent includes all of them, including privileged ones. /// This intent includes all of them, including privileged ones.
/// </summary> /// </summary>


+ 43
- 0
src/Discord.Net.Rest/API/Common/GuildScheduledEvent.cs View File

@@ -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; }
}
}

+ 15
- 0
src/Discord.Net.Rest/API/Common/GuildScheduledEventEntityMetadata.cs View File

@@ -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; }
}
}

+ 19
- 0
src/Discord.Net.Rest/API/Common/GuildScheduledEventUser.cs View File

@@ -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; }
}
}

+ 29
- 0
src/Discord.Net.Rest/API/Rest/CreateGuildScheduledEventParams.cs View File

@@ -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; }
}
}

+ 15
- 0
src/Discord.Net.Rest/API/Rest/GetEventUsersParams.cs View File

@@ -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; }
}
}

+ 31
- 0
src/Discord.Net.Rest/API/Rest/ModifyGuildScheduledEventParams.cs View File

@@ -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; }
}
}

+ 99
- 0
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -1909,6 +1909,105 @@ namespace Discord.API
} }
#endregion #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 #region Users
public async Task<User> GetUserAsync(ulong userId, RequestOptions options = null) public async Task<User> GetUserAsync(ulong userId, RequestOptions options = null)
{ {


+ 230
- 1
src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs View File

@@ -649,6 +649,235 @@ namespace Discord.Rest


public static async Task DeleteStickerAsync(BaseDiscordClient client, ulong guildId, ISticker sticker, RequestOptions options = null) public static async Task DeleteStickerAsync(BaseDiscordClient client, ulong guildId, ISticker sticker, RequestOptions options = null)
=> await client.ApiClient.DeleteStickerAsync(guildId, sticker.Id, options).ConfigureAwait(false); => 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
} }
} }

+ 71
- 0
src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs View File

@@ -1109,6 +1109,65 @@ namespace Discord.Rest
=> sticker.DeleteAsync(options); => sticker.DeleteAsync(options);
#endregion #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 #region IGuild
/// <inheritdoc /> /// <inheritdoc />
bool IGuild.Available => Available; bool IGuild.Available => Available;
@@ -1121,6 +1180,18 @@ namespace Discord.Rest


IReadOnlyCollection<ICustomSticker> IGuild.Stickers => Stickers; 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 /> /// <inheritdoc />
async Task<IReadOnlyCollection<IBan>> IGuild.GetBansAsync(RequestOptions options) async Task<IReadOnlyCollection<IBan>> IGuild.GetBansAsync(RequestOptions options)
=> await GetBansAsync(options).ConfigureAwait(false); => await GetBansAsync(options).ConfigureAwait(false);


+ 188
- 0
src/Discord.Net.Rest/Entities/Guilds/RestGuildEvent.cs View File

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

+ 13
- 0
src/Discord.Net.Rest/Entities/Users/RestUser.cs View File

@@ -4,6 +4,7 @@ using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Threading.Tasks; using System.Threading.Tasks;
using Model = Discord.API.User; using Model = Discord.API.User;
using EventUserModel = Discord.API.GuildScheduledEventUser;


namespace Discord.Rest namespace Discord.Rest
{ {
@@ -62,6 +63,18 @@ namespace Discord.Rest
entity.Update(model); entity.Update(model);
return entity; 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) internal virtual void Update(Model model)
{ {
if (model.Avatar.IsSpecified) if (model.Avatar.IsSpecified)


+ 1
- 1
src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs View File

@@ -27,7 +27,7 @@ namespace Discord.Net.Converters


public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{ {
throw new NotImplementedException();
writer.WriteValue(((DateTimeOffset)value).ToString("O"));
} }
} }
} }

+ 3
- 0
src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs View File

@@ -28,5 +28,8 @@ namespace Discord.API.Gateway


[JsonProperty("threads")] [JsonProperty("threads")]
public new Channel[] Threads { get; set; } public new Channel[] Threads { get; set; }

[JsonProperty("guild_scheduled_events")]
public GuildScheduledEvent[] GuildScheduledEvents { get; set; }
} }
} }

+ 55
- 0
src/Discord.Net.WebSocket/BaseSocketClient.Events.cs View File

@@ -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>>(); internal readonly AsyncEvent<Func<Cacheable<SocketGuildUser, ulong>, SocketGuild, Task>> _guildJoinRequestDeletedEvent = new AsyncEvent<Func<Cacheable<SocketGuildUser, ulong>, SocketGuild, Task>>();
#endregion #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 #region Users
/// <summary> Fired when a user joins a guild. </summary> /// <summary> Fired when a user joins a guild. </summary>
public event Func<SocketGuildUser, Task> UserJoined public event Func<SocketGuildUser, Task> UserJoined


+ 6
- 0
src/Discord.Net.WebSocket/DiscordShardedClient.cs View File

@@ -486,6 +486,12 @@ namespace Discord.WebSocket
client.GuildStickerDeleted += (sticker) => _guildStickerDeleted.InvokeAsync(sticker); client.GuildStickerDeleted += (sticker) => _guildStickerDeleted.InvokeAsync(sticker);
client.GuildStickerUpdated += (before, after) => _guildStickerUpdated.InvokeAsync(before, after); client.GuildStickerUpdated += (before, after) => _guildStickerUpdated.InvokeAsync(before, after);
client.GuildJoinRequestDeleted += (userId, guildId) => _guildJoinRequestDeletedEvent.InvokeAsync(userId, guildId); 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 #endregion




+ 75
- 3
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -2549,19 +2549,91 @@ namespace Discord.WebSocket
switch (type) switch (type)
{ {
case "STAGE_INSTANCE_CREATE": case "STAGE_INSTANCE_CREATE":
await TimedInvokeAsync(_stageStarted, nameof(StageStarted), stageChannel);
await TimedInvokeAsync(_stageStarted, nameof(StageStarted), stageChannel).ConfigureAwait(false);
return; return;
case "STAGE_INSTANCE_DELETE": case "STAGE_INSTANCE_DELETE":
await TimedInvokeAsync(_stageEnded, nameof(StageEnded), stageChannel);
await TimedInvokeAsync(_stageEnded, nameof(StageEnded), stageChannel).ConfigureAwait(false);
return; return;
case "STAGE_INSTANCE_UPDATE": case "STAGE_INSTANCE_UPDATE":
await TimedInvokeAsync(_stageUpdated, nameof(StageUpdated), before, stageChannel);
await TimedInvokeAsync(_stageUpdated, nameof(StageUpdated), before, stageChannel).ConfigureAwait(false);
return; return;
} }
} }
break; break;
#endregion #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) #region Ignored (User only)
case "CHANNEL_PINS_ACK": case "CHANNEL_PINS_ACK":
await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_ACK)").ConfigureAwait(false); await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_ACK)").ConfigureAwait(false);


+ 143
- 1
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs View File

@@ -20,6 +20,7 @@ using RoleModel = Discord.API.Role;
using UserModel = Discord.API.User; using UserModel = Discord.API.User;
using VoiceStateModel = Discord.API.VoiceState; using VoiceStateModel = Discord.API.VoiceState;
using StickerModel = Discord.API.Sticker; using StickerModel = Discord.API.Sticker;
using EventModel = Discord.API.GuildScheduledEvent;
using System.IO; using System.IO;


namespace Discord.WebSocket namespace Discord.WebSocket
@@ -40,6 +41,7 @@ namespace Discord.WebSocket
private ConcurrentDictionary<ulong, SocketRole> _roles; private ConcurrentDictionary<ulong, SocketRole> _roles;
private ConcurrentDictionary<ulong, SocketVoiceState> _voiceStates; private ConcurrentDictionary<ulong, SocketVoiceState> _voiceStates;
private ConcurrentDictionary<ulong, SocketCustomSticker> _stickers; private ConcurrentDictionary<ulong, SocketCustomSticker> _stickers;
private ConcurrentDictionary<ulong, SocketGuildEvent> _events;
private ImmutableArray<GuildEmote> _emotes; private ImmutableArray<GuildEmote> _emotes;


private AudioClient _audioClient; private AudioClient _audioClient;
@@ -364,6 +366,17 @@ namespace Discord.WebSocket
/// </returns> /// </returns>
public IReadOnlyCollection<SocketRole> Roles => _roles.ToReadOnlyCollection(); 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) internal SocketGuild(DiscordSocketClient client, ulong id)
: base(client, id) : base(client, id)
{ {
@@ -381,6 +394,8 @@ namespace Discord.WebSocket
IsAvailable = !(model.Unavailable ?? false); IsAvailable = !(model.Unavailable ?? false);
if (!IsAvailable) if (!IsAvailable)
{ {
if(_events == null)
_events = new ConcurrentDictionary<ulong, SocketGuildEvent>();
if (_channels == null) if (_channels == null)
_channels = new ConcurrentDictionary<ulong, SocketGuildChannel>(); _channels = new ConcurrentDictionary<ulong, SocketGuildChannel>();
if (_members == null) if (_members == null)
@@ -449,7 +464,16 @@ namespace Discord.WebSocket
} }
_voiceStates = voiceStates; _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>(); _syncPromise = new TaskCompletionSource<bool>();
_downloaderPromise = new TaskCompletionSource<bool>(); _downloaderPromise = new TaskCompletionSource<bool>();
@@ -1191,6 +1215,115 @@ namespace Discord.WebSocket
=> GuildHelper.SearchUsersAsync(this, Discord, query, limit, options); => GuildHelper.SearchUsersAsync(this, Discord, query, limit, options);
#endregion #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 #region Audit logs
/// <summary> /// <summary>
/// Gets the specified number of audit log entries for this guild. /// Gets the specified number of audit log entries for this guild.
@@ -1625,6 +1758,15 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
IReadOnlyCollection<ICustomSticker> IGuild.Stickers => Stickers; IReadOnlyCollection<ICustomSticker> IGuild.Stickers => Stickers;
/// <inheritdoc /> /// <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) async Task<IReadOnlyCollection<IBan>> IGuild.GetBansAsync(RequestOptions options)
=> await GetBansAsync(options).ConfigureAwait(false); => await GetBansAsync(options).ConfigureAwait(false);
/// <inheritdoc/> /// <inheritdoc/>


+ 216
- 0
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs View File

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

+ 2
- 0
test/Discord.Net.Tests.Unit/GuildPermissionsTests.cs View File

@@ -94,6 +94,7 @@ namespace Discord
AssertFlag(() => new GuildPermissions(manageEmojisAndStickers: true), GuildPermission.ManageEmojisAndStickers); AssertFlag(() => new GuildPermissions(manageEmojisAndStickers: true), GuildPermission.ManageEmojisAndStickers);
AssertFlag(() => new GuildPermissions(useApplicationCommands: true), GuildPermission.UseApplicationCommands); AssertFlag(() => new GuildPermissions(useApplicationCommands: true), GuildPermission.UseApplicationCommands);
AssertFlag(() => new GuildPermissions(requestToSpeak: true), GuildPermission.RequestToSpeak); AssertFlag(() => new GuildPermissions(requestToSpeak: true), GuildPermission.RequestToSpeak);
AssertFlag(() => new GuildPermissions(manageEvents: true), GuildPermission.ManageEvents);
AssertFlag(() => new GuildPermissions(manageThreads: true), GuildPermission.ManageThreads); AssertFlag(() => new GuildPermissions(manageThreads: true), GuildPermission.ManageThreads);
AssertFlag(() => new GuildPermissions(createPublicThreads: true), GuildPermission.CreatePublicThreads); AssertFlag(() => new GuildPermissions(createPublicThreads: true), GuildPermission.CreatePublicThreads);
AssertFlag(() => new GuildPermissions(createPrivateThreads: true), GuildPermission.CreatePrivateThreads); 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.ManageEmojisAndStickers, x => x.ManageEmojisAndStickers, (p, enable) => p.Modify(manageEmojisAndStickers: enable));
AssertUtil(GuildPermission.UseApplicationCommands, x => x.UseApplicationCommands, (p, enable) => p.Modify(useApplicationCommands: 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.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.ManageThreads, x => x.ManageThreads, (p, enable) => p.Modify(manageThreads: enable));
AssertUtil(GuildPermission.CreatePublicThreads, x => x.CreatePublicThreads, (p, enable) => p.Modify(createPublicThreads: 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)); AssertUtil(GuildPermission.CreatePrivateThreads, x => x.CreatePrivateThreads, (p, enable) => p.Modify(createPrivateThreads: enable));


Loading…
Cancel
Save