[Feature] Guild Onboarding support (#2616)

* api models

* moar models

* complete models

* modelsss

* forgot to push

* oh lol forgot this too

* api & rest guild method

* revert VS being VS & formatting to file scoped namespace

* socket entities

* yup

* fix xml doc

* changes
This commit is contained in:
Misha133
2023-04-15 02:08:30 +03:00
committed by GitHub
parent 84431decfd
commit 3a8f76c4b1
18 changed files with 574 additions and 3 deletions

View File

@@ -1307,5 +1307,13 @@ namespace Discord
/// A task that represents the asynchronous creation operation. The task result contains the created <see cref="IAutoModRule"/>.
/// </returns>
Task<IAutoModRule> CreateAutoModRuleAsync(Action<AutoModRuleProperties> props, RequestOptions options = null);
/// <summary>
/// Gets the onboarding object configured for the guild.
/// </summary>
/// <returns>
/// A task that represents the asynchronous creation operation. The task result contains the created <see cref="IGuildOnboarding"/>.
/// </returns>
Task<IGuildOnboarding> GetOnboardingAsync(RequestOptions options = null);
}
}

View File

@@ -0,0 +1,17 @@
namespace Discord;
/// <summary>
/// Represents the guild onboarding option type.
/// </summary>
public enum GuildOnboardingPromptType
{
/// <summary>
/// The prompt accepts multiple choices.
/// </summary>
MultipleChoice = 0,
/// <summary>
/// The prompt uses a dropdown menu.
/// </summary>
Dropdown = 1,
}

View File

@@ -0,0 +1,34 @@
using System.Collections.Generic;
namespace Discord;
/// <summary>
/// Represents the guild onboarding flow.
/// </summary>
public interface IGuildOnboarding
{
/// <summary>
/// Gets the ID of the guild this onboarding is part of.
/// </summary>
ulong GuildId { get; }
/// <summary>
/// Gets the guild this onboarding is part of.
/// </summary>
IGuild Guild { get; }
/// <summary>
/// Gets prompts shown during onboarding and in customize community.
/// </summary>
IReadOnlyCollection<IGuildOnboardingPrompt> Prompts { get; }
/// <summary>
/// Gets IDs of channels that members get opted into automatically.
/// </summary>
IReadOnlyCollection<ulong> DefaultChannelIds { get; }
/// <summary>
/// Gets whether onboarding is enabled in the guild.
/// </summary>
bool IsEnabled { get; }
}

View File

@@ -0,0 +1,40 @@
using System.Collections.Generic;
namespace Discord;
/// <summary>
/// Represents the guild onboarding prompt.
/// </summary>
public interface IGuildOnboardingPrompt : ISnowflakeEntity
{
/// <summary>
/// Gets options available within the prompt.
/// </summary>
IReadOnlyCollection<IGuildOnboardingPromptOption> Options { get; }
/// <summary>
/// Gets the title of the prompt.
/// </summary>
string Title { get; }
/// <summary>
/// Indicates whether users are limited to selecting one option for the prompt.
/// </summary>
bool IsSingleSelect { get; }
/// <summary>
/// Indicates whether the prompt is required before a user completes the onboarding flow.
/// </summary>
bool IsRequired { get; }
/// <summary>
/// Indicates whether the prompt is present in the onboarding flow.
/// If <see langword="false"/>, the prompt will only appear in the Channels and Roles tab.
/// </summary>
bool IsInOnboarding { get; }
/// <summary>
/// Gets the type of the prompt.
/// </summary>
GuildOnboardingPromptType Type { get; }
}

View File

@@ -0,0 +1,34 @@
using System.Collections.Generic;
namespace Discord;
/// <summary>
/// Represents the guild onboarding prompt option.
/// </summary>
public interface IGuildOnboardingPromptOption : ISnowflakeEntity
{
/// <summary>
/// Gets IDs of channels a member is added to when the option is selected.
/// </summary>
IReadOnlyCollection<ulong> ChannelIds { get; }
/// <summary>
/// Gets IDs of roles assigned to a member when the option is selected.
/// </summary>
IReadOnlyCollection<ulong> RoleIds { get; }
/// <summary>
/// Gets the emoji of the option. <see langword="null"/> if none is set.
/// </summary>
IEmote Emoji { get; }
/// <summary>
/// Gets the title of the option.
/// </summary>
string Title { get; }
/// <summary>
/// Gets the description of the option. <see langword="null"/> if none is set.
/// </summary>
string Description { get; }
}

View File

@@ -0,0 +1,18 @@
using Newtonsoft.Json;
namespace Discord.API;
internal class GuildOnboarding
{
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
[JsonProperty("prompts")]
public GuildOnboardingPrompt[] Prompts { get; set; }
[JsonProperty("default_channel_ids")]
public ulong[] DefaultChannelIds { get; set; }
[JsonProperty("enabled")]
public bool Enabled { get; set; }
}

View File

@@ -0,0 +1,27 @@
using Newtonsoft.Json;
namespace Discord.API;
internal class GuildOnboardingPrompt
{
[JsonProperty("id")]
public ulong Id { get; set; }
[JsonProperty("options")]
public GuildOnboardingPromptOption[] Options { get; set; }
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("single_select")]
public bool IsSingleSelect { get; set; }
[JsonProperty("required")]
public bool IsRequired { get; set; }
[JsonProperty("in_onboarding")]
public bool IsInOnboarding { get; set; }
[JsonProperty("type")]
public GuildOnboardingPromptType Type { get; set; }
}

View File

@@ -0,0 +1,24 @@
using Newtonsoft.Json;
namespace Discord.API;
internal class GuildOnboardingPromptOption
{
[JsonProperty("id")]
public ulong Id { get; set; }
[JsonProperty("channel_ids")]
public ulong[] ChannelIds { get; set; }
[JsonProperty("role_ids")]
public ulong[] RoleIds { get; set; }
[JsonProperty("emoji")]
public Emoji Emoji { get; set; }
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("description")]
public Optional<string> Description { get; set; }
}

View File

@@ -2239,6 +2239,19 @@ namespace Discord.API
#endregion
#region Guild Onboarding
public async Task<GuildOnboarding> GetGuildOnboardingAsync(ulong guildId, RequestOptions options)
{
Preconditions.NotEqual(guildId, 0, nameof(guildId));
options = RequestOptions.CreateOrClone(options);
return await SendAsync<GuildOnboarding>("GET", () => $"guilds/{guildId}/onboarding", new BucketIds(guildId: guildId), options: options);
}
#endregion
#region Users
public async Task<User> GetUserAsync(ulong userId, RequestOptions options = null)
{

View File

@@ -193,7 +193,7 @@ namespace Discord.Rest
},
start: fromUserId,
count: limit
);
);
}
public static async Task<RestBan> GetBanAsync(IGuild guild, BaseDiscordClient client, ulong userId, RequestOptions options)
@@ -427,7 +427,7 @@ namespace Discord.Rest
}
public static async Task DeleteIntegrationAsync(IGuild guild, BaseDiscordClient client, ulong id,
RequestOptions options) =>
await client.ApiClient.DeleteIntegrationAsync(guild.Id, id, options).ConfigureAwait(false);
await client.ApiClient.DeleteIntegrationAsync(guild.Id, id, options).ConfigureAwait(false);
#endregion
#region Interactions
@@ -810,7 +810,7 @@ namespace Discord.Rest
}
public static IAsyncEnumerable<IReadOnlyCollection<RestUser>> GetEventUsersAsync(BaseDiscordClient client, IGuildScheduledEvent guildEvent,
ulong? fromUserId, int? limit, RequestOptions options)
ulong? fromUserId, int? limit, RequestOptions options)
{
return new PagedAsyncEnumerable<RestUser>(
DiscordConfig.MaxGuildEventUsersPerBatch,
@@ -1254,5 +1254,12 @@ namespace Discord.Rest
public static Task DeleteRuleAsync(BaseDiscordClient client, IAutoModRule rule, RequestOptions options)
=> client.ApiClient.DeleteGuildAutoModRuleAsync(rule.GuildId, rule.Id, options);
#endregion
#region Onboarding
public static async Task<GuildOnboarding> GetGuildOnboardingAsync(IGuild guild, BaseDiscordClient client, RequestOptions options)
=> await client.ApiClient.GetGuildOnboardingAsync(guild.Id, options);
#endregion
}
}

View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Model = Discord.API.GuildOnboarding;
namespace Discord.Rest;
/// <inheritdoc />
public class RestGuildOnboarding : IGuildOnboarding
{
/// <inheritdoc />
public ulong GuildId { get; private set; }
/// <inheritdoc cref="IGuildOnboarding.Guild" />
public RestGuild Guild { get; private set; }
/// <inheritdoc />
public IReadOnlyCollection<ulong> DefaultChannelIds { get; private set; }
/// <inheritdoc />
public bool IsEnabled { get; private set; }
/// <inheritdoc cref="IGuildOnboarding.Prompts"/>
public IReadOnlyCollection<RestGuildOnboardingPrompt> Prompts { get; private set; }
internal RestGuildOnboarding(BaseDiscordClient discord, Model model, RestGuild guild = null)
{
GuildId = model.GuildId;
DefaultChannelIds = model.DefaultChannelIds.ToImmutableArray();
IsEnabled = model.Enabled;
Guild = guild;
Prompts = model.Prompts.Select(prompt => new RestGuildOnboardingPrompt(discord, prompt.Id, prompt)).ToImmutableArray();
}
#region IGuildOnboarding
/// <inheritdoc />
IReadOnlyCollection<IGuildOnboardingPrompt> IGuildOnboarding.Prompts => Prompts;
/// <inheritdoc />
IGuild IGuildOnboarding.Guild => Guild;
#endregion
}

View File

@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Model = Discord.API.GuildOnboardingPrompt;
namespace Discord.Rest;
/// <inheritdoc cref="IGuildOnboardingPrompt"/>
public class RestGuildOnboardingPrompt : RestEntity<ulong>, IGuildOnboardingPrompt
{
/// <inheritdoc />
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);
/// <inheritdoc cref="IGuildOnboardingPrompt.Options"/>
public IReadOnlyCollection<RestGuildOnboardingPromptOption> Options { get; private set; }
/// <inheritdoc />
public string Title { get; private set; }
/// <inheritdoc />
public bool IsSingleSelect { get; private set; }
/// <inheritdoc />
public bool IsRequired { get; private set; }
/// <inheritdoc />
public bool IsInOnboarding { get; private set; }
/// <inheritdoc />
public GuildOnboardingPromptType Type { get; private set; }
internal RestGuildOnboardingPrompt(BaseDiscordClient discord, ulong id, Model model) : base(discord, id)
{
Title = model.Title;
IsSingleSelect = model.IsSingleSelect;
IsInOnboarding = model.IsInOnboarding;
IsRequired = model.IsRequired;
Type = model.Type;
Options = model.Options.Select(option => new RestGuildOnboardingPromptOption(discord, option.Id, option)).ToImmutableArray();
}
#region IGuildOnboardingPrompt
/// <inheritdoc />
IReadOnlyCollection<IGuildOnboardingPromptOption> IGuildOnboardingPrompt.Options => Options;
#endregion
}

View File

@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Model = Discord.API.GuildOnboardingPromptOption;
namespace Discord.Rest;
/// <inheritdoc cref="IGuildOnboardingPromptOption"/>
public class RestGuildOnboardingPromptOption : RestEntity<ulong>, IGuildOnboardingPromptOption
{
/// <inheritdoc />
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);
/// <inheritdoc />
public IReadOnlyCollection<ulong> ChannelIds { get; private set; }
/// <inheritdoc />
public IReadOnlyCollection<ulong> RoleIds { get; private set; }
/// <inheritdoc />
public IEmote Emoji { get; private set; }
/// <inheritdoc />
public string Title { get; private set; }
/// <inheritdoc />
public string Description { get; private set; }
internal RestGuildOnboardingPromptOption(BaseDiscordClient discord, ulong id, Model model) : base(discord, id)
{
ChannelIds = model.ChannelIds.ToImmutableArray();
RoleIds = model.RoleIds.ToImmutableArray();
Title = model.Title;
Description = model.Description.IsSpecified ? model.Description.Value : null;
if (model.Emoji.Id.HasValue)
{
Emoji = new Emote(model.Emoji.Id.Value, model.Emoji.Name, model.Emoji.Animated ?? false);
}
else if (!string.IsNullOrWhiteSpace(model.Emoji.Name))
{
Emoji = new Emoji(model.Emoji.Name);
}
else
{
Emoji = null;
}
}
}

View File

@@ -1234,6 +1234,18 @@ namespace Discord.Rest
}
#endregion
#region Onboarding
/// <inheritdoc cref="IGuild.GetOnboardingAsync"/>
public async Task<RestGuildOnboarding> GetOnboardingAsync(RequestOptions options = null)
{
var model = await GuildHelper.GetGuildOnboardingAsync(this, Discord, options);
return new RestGuildOnboarding(Discord, model, this);
}
#endregion
#region IGuild
@@ -1593,6 +1605,10 @@ namespace Discord.Rest
async Task<IAutoModRule> IGuild.CreateAutoModRuleAsync(Action<AutoModRuleProperties> props, RequestOptions options)
=> await CreateAutoModRuleAsync(props, options).ConfigureAwait(false);
/// <inheritdoc/>
async Task<IGuildOnboarding> IGuild.GetOnboardingAsync(RequestOptions options)
=> await GetOnboardingAsync(options);
#endregion
}
}

View File

@@ -0,0 +1,53 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Model = Discord.API.GuildOnboarding;
namespace Discord.WebSocket;
/// <inheritdoc />
public class SocketGuildOnboarding : IGuildOnboarding
{
/// <inheritdoc />
public ulong GuildId { get; private set; }
/// <inheritdoc cref="IGuildOnboarding.Guild"/>
public SocketGuild Guild { get; private set; }
/// <inheritdoc cref="IGuildOnboarding.Prompts"/>
public IReadOnlyCollection<SocketGuildOnboardingPrompt> Prompts { get; private set; }
/// <inheritdoc />
public IReadOnlyCollection<ulong> DefaultChannelIds { get; private set; }
/// <summary>
/// Gets channels members get opted in automatically.
/// </summary>
public IReadOnlyCollection<SocketGuildChannel> DefaultChannels { get; private set; }
/// <inheritdoc />
public bool IsEnabled { get; private set; }
internal SocketGuildOnboarding(DiscordSocketClient discord, Model model, SocketGuild guild)
{
GuildId = model.GuildId;
Guild = guild;
IsEnabled = model.Enabled;
DefaultChannelIds = model.DefaultChannelIds;
DefaultChannels = model.DefaultChannelIds.Select(guild.GetChannel).ToImmutableArray();
Prompts = model.Prompts.Select(x => new SocketGuildOnboardingPrompt(discord, x.Id, x, guild)).ToImmutableArray();
}
#region IGuildOnboarding
/// <inheritdoc />
IGuild IGuildOnboarding.Guild => Guild;
/// <inheritdoc />
IReadOnlyCollection<IGuildOnboardingPrompt> IGuildOnboarding.Prompts => Prompts;
#endregion
}

View File

@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Model = Discord.API.GuildOnboardingPrompt;
namespace Discord.WebSocket;
/// <inheritdoc cref="IGuildOnboardingPrompt"/>
public class SocketGuildOnboardingPrompt : SocketEntity<ulong>, IGuildOnboardingPrompt
{
/// <inheritdoc />
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);
/// <inheritdoc cref="IGuildOnboardingPrompt.Options"/>
public IReadOnlyCollection<SocketGuildOnboardingPromptOption> Options { get; private set; }
/// <inheritdoc />
public string Title { get; private set; }
/// <inheritdoc />
public bool IsSingleSelect { get; private set; }
/// <inheritdoc />
public bool IsRequired { get; private set; }
/// <inheritdoc />
public bool IsInOnboarding { get; private set; }
/// <inheritdoc />
public GuildOnboardingPromptType Type { get; private set; }
internal SocketGuildOnboardingPrompt(DiscordSocketClient discord, ulong id, Model model, SocketGuild guild) : base(discord, id)
{
Title = model.Title;
IsSingleSelect = model.IsSingleSelect;
IsInOnboarding = model.IsInOnboarding;
IsRequired = model.IsRequired;
Type = model.Type;
Options = model.Options.Select(option => new SocketGuildOnboardingPromptOption(discord, option.Id, option, guild)).ToImmutableArray();
}
#region IGuildOnboardingPrompt
/// <inheritdoc />
IReadOnlyCollection<IGuildOnboardingPromptOption> IGuildOnboardingPrompt.Options => Options;
#endregion
}

View File

@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Model = Discord.API.GuildOnboardingPromptOption;
namespace Discord.WebSocket;
/// <inheritdoc cref="IGuildOnboardingPromptOption"/>
public class SocketGuildOnboardingPromptOption : SocketEntity<ulong>, IGuildOnboardingPromptOption
{
/// <inheritdoc />
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);
/// <inheritdoc />
public IReadOnlyCollection<ulong> ChannelIds { get; private set; }
/// <summary>
/// Gets channels a member is added to when the option is selected.
/// </summary>
public IReadOnlyCollection<SocketGuildChannel> Channels { get; private set; }
/// <inheritdoc />
public IReadOnlyCollection<ulong> RoleIds { get; private set; }
/// <summary>
/// Gets roles assigned to a member when the option is selected.
/// </summary>
public IReadOnlyCollection<SocketRole> Roles { get; private set; }
/// <inheritdoc />
public IEmote Emoji { get; private set; }
/// <inheritdoc />
public string Title { get; private set; }
/// <inheritdoc />
public string Description { get; private set; }
internal SocketGuildOnboardingPromptOption(DiscordSocketClient discord, ulong id, Model model, SocketGuild guild) : base(discord, id)
{
ChannelIds = model.ChannelIds.ToImmutableArray();
RoleIds = model.RoleIds.ToImmutableArray();
Title = model.Title;
Description = model.Description.IsSpecified ? model.Description.Value : null;
if (model.Emoji.Id.HasValue)
{
Emoji = new Emote(model.Emoji.Id.Value, model.Emoji.Name, model.Emoji.Animated ?? false);
}
else if (!string.IsNullOrWhiteSpace(model.Emoji.Name))
{
Emoji = new Emoji(model.Emoji.Name);
}
else
{
Emoji = null;
}
Roles = model.RoleIds.Select(guild.GetRole).ToImmutableArray();
Channels = model.ChannelIds.Select(guild.GetChannel).ToImmutableArray();
}
}

View File

@@ -1,6 +1,7 @@
using Discord.API.Gateway;
using Discord.Audio;
using Discord.Rest;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -11,6 +12,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AutoModRuleModel = Discord.API.AutoModerationRule;
using ChannelModel = Discord.API.Channel;
using EmojiUpdateModel = Discord.API.Gateway.GuildEmojiUpdateEvent;
@@ -1908,6 +1910,18 @@ namespace Discord.WebSocket
#endregion
#region Onboarding
/// <inheritdoc cref="IGuild.GetOnboardingAsync"/>
public async Task<SocketGuildOnboarding> GetOnboardingAsync(RequestOptions options = null)
{
var model = await GuildHelper.GetGuildOnboardingAsync(this, Discord, options);
return new SocketGuildOnboarding(Discord, model, this);
}
#endregion
#region IGuild
/// <inheritdoc />
ulong? IGuild.AFKChannelId => AFKChannelId;
@@ -2166,6 +2180,10 @@ namespace Discord.WebSocket
async Task<IAutoModRule> IGuild.CreateAutoModRuleAsync(Action<AutoModRuleProperties> props, RequestOptions options)
=> await CreateAutoModRuleAsync(props, options).ConfigureAwait(false);
/// <inheritdoc/>
async Task<IGuildOnboarding> IGuild.GetOnboardingAsync(RequestOptions options)
=> await GetOnboardingAsync(options);
#endregion
}
}