diff --git a/DisCatSharp.Interactivity/Extensions/MessageExtensions.cs b/DisCatSharp.Interactivity/Extensions/MessageExtensions.cs
index 735170684..e88e95b7c 100644
--- a/DisCatSharp.Interactivity/Extensions/MessageExtensions.cs
+++ b/DisCatSharp.Interactivity/Extensions/MessageExtensions.cs
@@ -1,243 +1,244 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using DisCatSharp.Entities;
+using DisCatSharp.Enums;
using DisCatSharp.EventArgs;
using DisCatSharp.Interactivity.Enums;
using DisCatSharp.Interactivity.EventHandling;
namespace DisCatSharp.Interactivity.Extensions;
///
/// Interactivity extension methods for .
///
public static class MessageExtensions
{
///
/// Waits for the next message that has the same author and channel as this message.
///
/// Original message.
/// Overrides the timeout set in
public static Task> GetNextMessageAsync(this DiscordMessage message, TimeSpan? timeoutOverride = null)
=> message.Channel.GetNextMessageAsync(message.Author, timeoutOverride);
///
/// Waits for the next message with the same author and channel as this message, which also satisfies a predicate.
///
/// Original message.
/// A predicate that should return if a message matches.
/// Overrides the timeout set in
public static Task> GetNextMessageAsync(this DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null)
=> message.Channel.GetNextMessageAsync(msg => msg.Author.Id == message.Author.Id && message.ChannelId == msg.ChannelId && predicate(msg), timeoutOverride);
///
/// Waits for any button to be pressed on the specified message.
///
/// The message to wait on.
public static Task> WaitForButtonAsync(this DiscordMessage message)
=> GetInteractivity(message).WaitForButtonAsync(message);
///
/// Waits for any button to be pressed on the specified message.
///
/// The message to wait on.
/// Overrides the timeout set in
public static Task> WaitForButtonAsync(this DiscordMessage message, TimeSpan? timeoutOverride = null)
=> GetInteractivity(message).WaitForButtonAsync(message, timeoutOverride);
///
/// Waits for any button to be pressed on the specified message.
///
/// The message to wait on.
/// A custom cancellation token that can be cancelled at any point.
public static Task> WaitForButtonAsync(this DiscordMessage message, CancellationToken token)
=> GetInteractivity(message).WaitForButtonAsync(message, token);
///
/// Waits for a button with the specified Id to be pressed on the specified message.
///
/// The message to wait on.
/// The Id of the button to wait for.
/// Overrides the timeout set in
public static Task> WaitForButtonAsync(this DiscordMessage message, string id, TimeSpan? timeoutOverride = null)
=> GetInteractivity(message).WaitForButtonAsync(message, id, timeoutOverride);
///
/// Waits for a button with the specified Id to be pressed on the specified message.
///
/// The message to wait on.
/// The Id of the button to wait for.
/// A custom cancellation token that can be cancelled at any point.
public static Task> WaitForButtonAsync(this DiscordMessage message, string id, CancellationToken token)
=> GetInteractivity(message).WaitForButtonAsync(message, id, token);
///
/// Waits for any button to be pressed on the specified message by the specified user.
///
/// The message to wait on.
/// The user to wait for button input from.
/// Overrides the timeout set in
public static Task> WaitForButtonAsync(this DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null)
=> GetInteractivity(message).WaitForButtonAsync(message, user, timeoutOverride);
///
/// Waits for any button to be pressed on the specified message by the specified user.
///
/// The message to wait on.
/// The user to wait for button input from.
/// A custom cancellation token that can be cancelled at any point.
public static Task> WaitForButtonAsync(this DiscordMessage message, DiscordUser user, CancellationToken token)
=> GetInteractivity(message).WaitForButtonAsync(message, user, token);
///
/// Waits for any button to be interacted with.
///
/// The message to wait on.
/// The predicate to filter interactions by.
/// Override the timeout specified in
public static Task> WaitForButtonAsync(this DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null)
=> GetInteractivity(message).WaitForButtonAsync(message, predicate, timeoutOverride);
///
/// Waits for any button to be interacted with.
///
/// The message to wait on.
/// The predicate to filter interactions by.
/// A token to cancel interactivity with at any time. Pass to wait indefinitely.
public static Task> WaitForButtonAsync(this DiscordMessage message, Func predicate, CancellationToken token)
=> GetInteractivity(message).WaitForButtonAsync(message, predicate, token);
///
/// Waits for any dropdown to be interacted with.
///
/// The message to wait for.
/// A filter predicate.
/// Override the timeout period specified in .
/// Thrown when the message doesn't contain any dropdowns
- public static Task> WaitForSelectAsync(this DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null)
- => GetInteractivity(message).WaitForSelectAsync(message, predicate, timeoutOverride);
+ public static Task> WaitForSelectAsync(this DiscordMessage message, Func predicate, ComponentType selectType, TimeSpan? timeoutOverride = null)
+ => GetInteractivity(message).WaitForSelectAsync(message, predicate, selectType, timeoutOverride);
///
/// Waits for any dropdown to be interacted with.
///
/// The message to wait for.
/// A filter predicate.
/// A token that can be used to cancel interactivity. Pass to wait indefinitely.
/// Thrown when the message doesn't contain any dropdowns
- public static Task> WaitForSelectAsync(this DiscordMessage message, Func predicate, CancellationToken token)
- => GetInteractivity(message).WaitForSelectAsync(message, predicate, token);
+ public static Task> WaitForSelectAsync(this DiscordMessage message, Func predicate, ComponentType selectType, CancellationToken token)
+ => GetInteractivity(message).WaitForSelectAsync(message, predicate, selectType, token);
///
/// Waits for a dropdown to be interacted with.
///
/// The message to wait on.
/// The Id of the dropdown to wait for.
/// Overrides the timeout set in
- public static Task> WaitForSelectAsync(this DiscordMessage message, string id, TimeSpan? timeoutOverride = null)
- => GetInteractivity(message).WaitForSelectAsync(message, id, timeoutOverride);
+ public static Task> WaitForSelectAsync(this DiscordMessage message, string id, ComponentType selectType, TimeSpan? timeoutOverride = null)
+ => GetInteractivity(message).WaitForSelectAsync(message, id, selectType, timeoutOverride);
///
/// Waits for a dropdown to be interacted with.
///
/// The message to wait on.
/// The Id of the dropdown to wait for.
/// A custom cancellation token that can be cancelled at any point.
- public static Task> WaitForSelectAsync(this DiscordMessage message, string id, CancellationToken token)
- => GetInteractivity(message).WaitForSelectAsync(message, id, token);
+ public static Task> WaitForSelectAsync(this DiscordMessage message, string id, ComponentType selectType, CancellationToken token)
+ => GetInteractivity(message).WaitForSelectAsync(message, id, selectType, token);
///
/// Waits for a dropdown to be interacted with by the specified user.
///
/// The message to wait on.
/// The user to wait for.
/// The Id of the dropdown to wait for.
///
- public static Task> WaitForSelectAsync(this DiscordMessage message, DiscordUser user, string id, TimeSpan? timeoutOverride = null)
- => GetInteractivity(message).WaitForSelectAsync(message, user, id, timeoutOverride);
+ public static Task> WaitForSelectAsync(this DiscordMessage message, DiscordUser user, string id, ComponentType selectType, TimeSpan? timeoutOverride = null)
+ => GetInteractivity(message).WaitForSelectAsync(message, user, id, selectType, timeoutOverride);
///
/// Waits for a dropdown to be interacted with by the specified user.
///
/// The message to wait on.
/// The user to wait for.
/// The Id of the dropdown to wait for.
/// A custom cancellation token that can be cancelled at any point.
- public static Task> WaitForSelectAsync(this DiscordMessage message, DiscordUser user, string id, CancellationToken token)
- => GetInteractivity(message).WaitForSelectAsync(message, user, id, token);
+ public static Task> WaitForSelectAsync(this DiscordMessage message, DiscordUser user, string id, ComponentType selectType, CancellationToken token)
+ => GetInteractivity(message).WaitForSelectAsync(message, user, id, selectType, token);
///
/// Waits for a reaction on this message from a specific user.
///
/// Target message.
/// The target user.
/// Overrides the timeout set in
/// Thrown if interactivity is not enabled for the client associated with the message.
public static Task> WaitForReactionAsync(this DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null)
=> GetInteractivity(message).WaitForReactionAsync(message, user, timeoutOverride);
///
/// Waits for a specific reaction on this message from the specified user.
///
/// Target message.
/// The target user.
/// The target emoji.
/// Overrides the timeout set in
/// Thrown if interactivity is not enabled for the client associated with the message.
public static Task> WaitForReactionAsync(this DiscordMessage message, DiscordUser user, DiscordEmoji emoji, TimeSpan? timeoutOverride = null)
=> GetInteractivity(message).WaitForReactionAsync(e => e.Emoji == emoji, message, user, timeoutOverride);
///
/// Collects all reactions on this message within the timeout duration.
///
/// The message to collect reactions from.
/// Overrides the timeout set in
/// Thrown if interactivity is not enabled for the client associated with the message.
public static Task> CollectReactionsAsync(this DiscordMessage message, TimeSpan? timeoutOverride = null)
=> GetInteractivity(message).CollectReactionsAsync(message, timeoutOverride);
///
/// Begins a poll using this message.
///
/// Target message.
/// Options for this poll.
/// Overrides the action set in
/// Overrides the timeout set in
/// Thrown if interactivity is not enabled for the client associated with the message.
public static Task> DoPollAsync(this DiscordMessage message, IEnumerable emojis, PollBehaviour? behaviorOverride = null, TimeSpan? timeoutOverride = null)
=> GetInteractivity(message).DoPollAsync(message, emojis, behaviorOverride, timeoutOverride);
///
/// Retrieves an interactivity instance from a message instance.
///
internal static InteractivityExtension GetInteractivity(DiscordMessage message)
{
var client = (DiscordClient)message.Discord;
var interactivity = client.GetInteractivity();
return interactivity ?? throw new InvalidOperationException($"Interactivity is not enabled for this {(client.IsShard ? "shard" : "client")}.");
}
}
diff --git a/DisCatSharp.Interactivity/InteractivityExtension.cs b/DisCatSharp.Interactivity/InteractivityExtension.cs
index c0fa81fed..479973a02 100644
--- a/DisCatSharp.Interactivity/InteractivityExtension.cs
+++ b/DisCatSharp.Interactivity/InteractivityExtension.cs
@@ -1,971 +1,971 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DisCatSharp.Common.Utilities;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.EventArgs;
using DisCatSharp.Interactivity.Enums;
using DisCatSharp.Interactivity.EventHandling;
namespace DisCatSharp.Interactivity;
///
/// Extension class for DisCatSharp.Interactivity
///
public class InteractivityExtension : BaseExtension
{
///
/// Gets the config.
///
internal InteractivityConfiguration Config { get; }
private EventWaiter _messageCreatedWaiter;
private EventWaiter _messageReactionAddWaiter;
private EventWaiter _typingStartWaiter;
private EventWaiter _modalInteractionWaiter;
private EventWaiter _componentInteractionWaiter;
private ComponentEventWaiter _componentEventWaiter;
private ModalEventWaiter _modalEventWaiter;
private ReactionCollector _reactionCollector;
private Poller _poller;
private Paginator _paginator;
private ComponentPaginator _compPaginator;
///
/// Initializes a new instance of the class.
///
/// The configuration.
internal InteractivityExtension(InteractivityConfiguration cfg)
{
this.Config = new InteractivityConfiguration(cfg);
}
///
/// Setups the Interactivity Extension.
///
/// Discord client.
protected internal override void Setup(DiscordClient client)
{
this.Client = client;
this._messageCreatedWaiter = new EventWaiter(this.Client);
this._messageReactionAddWaiter = new EventWaiter(this.Client);
this._componentInteractionWaiter = new EventWaiter(this.Client);
this._modalInteractionWaiter = new EventWaiter(this.Client);
this._typingStartWaiter = new EventWaiter(this.Client);
this._poller = new Poller(this.Client);
this._reactionCollector = new ReactionCollector(this.Client);
this._paginator = new Paginator(this.Client);
this._compPaginator = new ComponentPaginator(this.Client, this.Config);
this._componentEventWaiter = new ComponentEventWaiter(this.Client, this.Config);
this._modalEventWaiter = new ModalEventWaiter(this.Client, this.Config);
}
///
/// Makes a poll and returns poll results.
///
/// Message to create poll on.
/// Emojis to use for this poll.
/// What to do when the poll ends.
/// Override timeout period.
///
public async Task> DoPollAsync(DiscordMessage m, IEnumerable emojis, PollBehaviour? behaviour = default, TimeSpan? timeout = null)
{
if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No reaction intents are enabled.");
if (!emojis.Any())
throw new ArgumentException("You need to provide at least one emoji for a poll!");
foreach (var em in emojis)
await m.CreateReactionAsync(em).ConfigureAwait(false);
var res = await this._poller.DoPollAsync(new PollRequest(m, timeout ?? this.Config.Timeout, emojis)).ConfigureAwait(false);
var pollBehaviour = behaviour ?? this.Config.PollBehaviour;
var thisMember = await m.Channel.Guild.GetMemberAsync(this.Client.CurrentUser.Id).ConfigureAwait(false);
if (pollBehaviour == PollBehaviour.DeleteEmojis && m.Channel.PermissionsFor(thisMember).HasPermission(Permissions.ManageMessages))
await m.DeleteAllReactionsAsync().ConfigureAwait(false);
return new ReadOnlyCollection(res.ToList());
}
///
/// Waits for any button in the specified collection to be pressed.
///
/// The message to wait on.
/// A collection of buttons to listen for.
/// Override the timeout period in .
/// A with the result of button that was pressed, if any.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public Task> WaitForButtonAsync(DiscordMessage message, IEnumerable buttons, TimeSpan? timeoutOverride = null)
=> this.WaitForButtonAsync(message, buttons, this.GetCancellationToken(timeoutOverride));
///
/// Waits for any button in the specified collection to be pressed.
///
/// The message to wait on.
/// A collection of buttons to listen for.
/// A custom cancellation token that can be cancelled at any point.
/// A with the result of button that was pressed, if any.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public async Task> WaitForButtonAsync(DiscordMessage message, IEnumerable buttons, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!buttons.Any())
throw new ArgumentException("You must specify at least one button to listen for.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button))
throw new ArgumentException("Provided Message does not contain any button components.");
var res = await this._componentEventWaiter
.WaitForMatchAsync(new ComponentMatchRequest(message,
c =>
c.Interaction.Data.ComponentType == ComponentType.Button &&
buttons.Any(b => b.CustomId == c.Id), token)).ConfigureAwait(false);
return new InteractivityResult(res is null, res);
}
///
/// Waits for a user modal submit.
///
/// The custom id of the modal to wait for.
/// Override the timeout period specified in .
/// A with the result of the modal.
public Task> WaitForModalAsync(string customId, TimeSpan? timeoutOverride = null)
=> this.WaitForModalAsync(customId, this.GetCancellationToken(timeoutOverride));
///
/// Waits for a user modal submit.
///
/// The custom id of the modal to wait for.
/// A custom cancellation token that can be cancelled at any point.
/// A with the result of the modal.
public async Task> WaitForModalAsync(string customId, CancellationToken token)
{
var result =
await this
._modalEventWaiter
.WaitForModalMatchAsync(new ModalMatchRequest(customId, c => c.Interaction.Type == InteractionType.ModalSubmit, token))
.ConfigureAwait(false);
return new InteractivityResult(result is null, result);
}
///
/// Waits for any button on the specified message to be pressed.
///
/// The message to wait for the button on.
/// Override the timeout period specified in .
/// A with the result of button that was pressed, if any.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public Task> WaitForButtonAsync(DiscordMessage message, TimeSpan? timeoutOverride = null)
=> this.WaitForButtonAsync(message, this.GetCancellationToken(timeoutOverride));
///
/// Waits for any button on the specified message to be pressed.
///
/// The message to wait for the button on.
/// A custom cancellation token that can be cancelled at any point.
/// A with the result of button that was pressed, if any.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public async Task> WaitForButtonAsync(DiscordMessage message, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button))
throw new ArgumentException("Message does not contain any button components.");
var ids = message.Components.SelectMany(m => m.Components).Select(c => c.CustomId);
var result =
await this
._componentEventWaiter
.WaitForMatchAsync(new ComponentMatchRequest(message, c => c.Interaction.Data.ComponentType == ComponentType.Button && ids.Contains(c.Id), token))
.ConfigureAwait(false);
return new InteractivityResult(result is null, result);
}
///
/// Waits for any button on the specified message to be pressed by the specified user.
///
/// The message to wait for the button on.
/// The user to wait for the button press from.
/// Override the timeout period specified in .
/// A with the result of button that was pressed, if any.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public Task> WaitForButtonAsync(DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null)
=> this.WaitForButtonAsync(message, user, this.GetCancellationToken(timeoutOverride));
///
/// Waits for any button on the specified message to be pressed by the specified user.
///
/// The message to wait for the button on.
/// The user to wait for the button press from.
/// A custom cancellation token that can be cancelled at any point.
/// A with the result of button that was pressed, if any.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public async Task> WaitForButtonAsync(DiscordMessage message, DiscordUser user, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button))
throw new ArgumentException("Message does not contain any button components.");
var result = await this
._componentEventWaiter
.WaitForMatchAsync(new ComponentMatchRequest(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.User == user, token))
.ConfigureAwait(false);
return new InteractivityResult(result is null, result);
}
///
/// Waits for a button with the specified Id to be pressed.
///
/// The message to wait for the button on.
/// The Id of the button to wait for.
/// Override the timeout period specified in .
/// A with the result of the operation.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public Task> WaitForButtonAsync(DiscordMessage message, string id, TimeSpan? timeoutOverride = null)
=> this.WaitForButtonAsync(message, id, this.GetCancellationToken(timeoutOverride));
///
/// Waits for a button with the specified Id to be pressed.
///
/// The message to wait for the button on.
/// The Id of the button to wait for.
/// Override the timeout period specified in .
/// A with the result of the operation.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public async Task> WaitForButtonAsync(DiscordMessage message, string id, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button))
throw new ArgumentException("Message does not contain any button components.");
if (!message.Components.SelectMany(c => c.Components).OfType().Any(c => c.CustomId == id))
throw new ArgumentException($"Message does not contain button with Id of '{id}'.");
var result = await this
._componentEventWaiter
.WaitForMatchAsync(new ComponentMatchRequest(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.Id == id, token))
.ConfigureAwait(false);
return new InteractivityResult(result is null, result);
}
///
/// Waits for any button to be interacted with.
///
/// The message to wait on.
/// The predicate to filter interactions by.
/// Override the timeout specified in
public Task> WaitForButtonAsync(DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null)
=> this.WaitForButtonAsync(message, predicate, this.GetCancellationToken(timeoutOverride));
///
/// Waits for any button to be interacted with.
///
/// The message to wait on.
/// The predicate to filter interactions by.
/// A token to cancel interactivity with at any time. Pass to wait indefinitely.
public async Task> WaitForButtonAsync(DiscordMessage message, Func predicate, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button))
throw new ArgumentException("Message does not contain any button components.");
var result = await this
._componentEventWaiter
.WaitForMatchAsync(new ComponentMatchRequest(message, c => c.Interaction.Data.ComponentType is ComponentType.Button && predicate(c), token))
.ConfigureAwait(false);
return new InteractivityResult(result is null, result);
}
///
/// Waits for any dropdown to be interacted with.
///
/// The message to wait for.
/// A filter predicate.
/// Override the timeout period specified in .
/// Thrown when the Provided message does not contain any dropdowns
- public Task> WaitForSelectAsync(DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null)
- => this.WaitForSelectAsync(message, predicate, this.GetCancellationToken(timeoutOverride));
+ public Task> WaitForSelectAsync(DiscordMessage message, Func predicate, ComponentType selectType, TimeSpan? timeoutOverride = null)
+ => this.WaitForSelectAsync(message, predicate, selectType, this.GetCancellationToken(timeoutOverride));
///
/// Waits for any dropdown to be interacted with.
///
/// The message to wait for.
/// A filter predicate.
/// A token that can be used to cancel interactivity. Pass to wait indefinitely.
/// Thrown when the Provided message does not contain any dropdowns
- public async Task> WaitForSelectAsync(DiscordMessage message, Func predicate, CancellationToken token)
+ public async Task> WaitForSelectAsync(DiscordMessage message, Func predicate, ComponentType selectType, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
- if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select))
+ if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type == selectType))
throw new ArgumentException("Message does not contain any select components.");
var result = await this
._componentEventWaiter
- .WaitForMatchAsync(new ComponentMatchRequest(message, c => c.Interaction.Data.ComponentType is ComponentType.Select && predicate(c), token))
+ .WaitForMatchAsync(new ComponentMatchRequest(message, c => c.Interaction.Data.ComponentType == selectType && predicate(c), token))
.ConfigureAwait(false);
return new InteractivityResult(result is null, result);
}
///
/// Waits for a dropdown to be interacted with.
///
/// This is here for backwards-compatibility and will internally create a cancellation token.
/// The message to wait on.
/// The Id of the dropdown to wait on.
/// Override the timeout period specified in .
/// Thrown when the message does not have any dropdowns or any dropdown with the specified Id.
- public Task> WaitForSelectAsync(DiscordMessage message, string id, TimeSpan? timeoutOverride = null)
- => this.WaitForSelectAsync(message, id, this.GetCancellationToken(timeoutOverride));
+ public Task> WaitForSelectAsync(DiscordMessage message, string id, ComponentType selectType, TimeSpan? timeoutOverride = null)
+ => this.WaitForSelectAsync(message, id, selectType, this.GetCancellationToken(timeoutOverride));
///
/// Waits for a dropdown to be interacted with.
///
/// The message to wait on.
/// The Id of the dropdown to wait on.
/// A custom cancellation token that can be cancelled at any point.
/// Thrown when the message does not have any dropdowns or any dropdown with the specified Id.
- public async Task> WaitForSelectAsync(DiscordMessage message, string id, CancellationToken token)
+ public async Task> WaitForSelectAsync(DiscordMessage message, string id, ComponentType selectType, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
- if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select))
+ if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type == selectType))
throw new ArgumentException("Message does not contain any select components.");
- if (message.Components.SelectMany(c => c.Components).OfType().All(c => c.CustomId != id))
+ if (message.Components.SelectMany(c => c.Components).OfType().All(c => c.CustomId != id))
throw new ArgumentException($"Message does not contain select component with Id of '{id}'.");
var result = await this
._componentEventWaiter
- .WaitForMatchAsync(new ComponentMatchRequest(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Select && c.Id == id, token))
+ .WaitForMatchAsync(new ComponentMatchRequest(message, (c) => c.Interaction.Data.ComponentType == selectType && c.Id == id, token))
.ConfigureAwait(false);
return new InteractivityResult(result is null, result);
}
///
/// Waits for a dropdown to be interacted with by a specific user.
///
/// The message to wait on.
/// The user to wait on.
/// The Id of the dropdown to wait on.
/// Override the timeout period specified in .
/// Thrown when the message does not have any dropdowns or any dropdown with the specified Id.
- public Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, string id, TimeSpan? timeoutOverride = null)
- => this.WaitForSelectAsync(message, user, id, this.GetCancellationToken(timeoutOverride));
+ public Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, string id, ComponentType selectType, TimeSpan? timeoutOverride = null)
+ => this.WaitForSelectAsync(message, user, id, selectType, this.GetCancellationToken(timeoutOverride));
///
/// Waits for a dropdown to be interacted with by a specific user.
///
/// The message to wait on.
/// The user to wait on.
/// The Id of the dropdown to wait on.
/// A custom cancellation token that can be cancelled at any point.
/// Thrown when the message does not have any dropdowns or any dropdown with the specified Id.
- public async Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, string id, CancellationToken token)
+ public async Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, string id, ComponentType selectType, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
- if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select))
+ if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type == selectType))
throw new ArgumentException("Message does not contain any select components.");
- if (message.Components.SelectMany(c => c.Components).OfType().All(c => c.CustomId != id))
+ if (message.Components.SelectMany(c => c.Components).OfType().All(c => c.CustomId != id))
throw new ArgumentException($"Message does not contain select with Id of '{id}'.");
var result = await this
._componentEventWaiter
.WaitForMatchAsync(new ComponentMatchRequest(message, (c) => c.Id == id && c.User == user, token)).ConfigureAwait(false);
return new InteractivityResult(result is null, result);
}
///
/// Waits for a specific message.
///
/// Predicate to match.
/// Override timeout period.
public async Task> WaitForMessageAsync(Func predicate,
TimeSpan? timeoutOverride = null)
{
if (!Utilities.HasMessageIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No message intents are enabled.");
var timeout = timeoutOverride ?? this.Config.Timeout;
var returns = await this._messageCreatedWaiter.WaitForMatchAsync(new MatchRequest(x => predicate(x.Message), timeout)).ConfigureAwait(false);
return new InteractivityResult(returns == null, returns?.Message);
}
///
/// Wait for a specific reaction.
///
/// Predicate to match.
/// Override timeout period.
public async Task> WaitForReactionAsync(Func predicate,
TimeSpan? timeoutOverride = null)
{
if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No reaction intents are enabled.");
var timeout = timeoutOverride ?? this.Config.Timeout;
var returns = await this._messageReactionAddWaiter.WaitForMatchAsync(new MatchRequest(predicate, timeout)).ConfigureAwait(false);
return new InteractivityResult(returns == null, returns);
}
///
/// Wait for a specific reaction.
/// For this Event you need the intent specified in
///
/// Message reaction was added to.
/// User that made the reaction.
/// Override timeout period.
public async Task> WaitForReactionAsync(DiscordMessage message, DiscordUser user,
TimeSpan? timeoutOverride = null)
=> await this.WaitForReactionAsync(x => x.User.Id == user.Id && x.Message.Id == message.Id, timeoutOverride).ConfigureAwait(false);
///
/// Waits for a specific reaction.
/// For this Event you need the intent specified in
///
/// Predicate to match.
/// Message reaction was added to.
/// User that made the reaction.
/// Override timeout period.
public async Task> WaitForReactionAsync(Func predicate,
DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null)
=> await this.WaitForReactionAsync(x => predicate(x) && x.User.Id == user.Id && x.Message.Id == message.Id, timeoutOverride).ConfigureAwait(false);
///
/// Waits for a specific reaction.
/// For this Event you need the intent specified in
///
/// predicate to match.
/// User that made the reaction.
/// Override timeout period.
public async Task> WaitForReactionAsync(Func predicate,
DiscordUser user, TimeSpan? timeoutOverride = null)
=> await this.WaitForReactionAsync(x => predicate(x) && x.User.Id == user.Id, timeoutOverride).ConfigureAwait(false);
///
/// Waits for a user to start typing.
///
/// User that starts typing.
/// Channel the user is typing in.
/// Override timeout period.
public async Task> WaitForUserTypingAsync(DiscordUser user,
DiscordChannel channel, TimeSpan? timeoutOverride = null)
{
if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No typing intents are enabled.");
var timeout = timeoutOverride ?? this.Config.Timeout;
var returns = await this._typingStartWaiter.WaitForMatchAsync(
new MatchRequest(x => x.User.Id == user.Id && x.Channel.Id == channel.Id, timeout))
.ConfigureAwait(false);
return new InteractivityResult(returns == null, returns);
}
///
/// Waits for a user to start typing.
///
/// User that starts typing.
/// Override timeout period.
public async Task> WaitForUserTypingAsync(DiscordUser user, TimeSpan? timeoutOverride = null)
{
if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No typing intents are enabled.");
var timeout = timeoutOverride ?? this.Config.Timeout;
var returns = await this._typingStartWaiter.WaitForMatchAsync(
new MatchRequest(x => x.User.Id == user.Id, timeout))
.ConfigureAwait(false);
return new InteractivityResult(returns == null, returns);
}
///
/// Waits for any user to start typing.
///
/// Channel to type in.
/// Override timeout period.
public async Task> WaitForTypingAsync(DiscordChannel channel, TimeSpan? timeoutOverride = null)
{
if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No typing intents are enabled.");
var timeout = timeoutOverride ?? this.Config.Timeout;
var returns = await this._typingStartWaiter.WaitForMatchAsync(
new MatchRequest(x => x.Channel.Id == channel.Id, timeout))
.ConfigureAwait(false);
return new InteractivityResult(returns == null, returns);
}
///
/// Collects reactions on a specific message.
///
/// Message to collect reactions on.
/// Override timeout period.
public async Task> CollectReactionsAsync(DiscordMessage m, TimeSpan? timeoutOverride = null)
{
if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No reaction intents are enabled.");
var timeout = timeoutOverride ?? this.Config.Timeout;
var collection = await this._reactionCollector.CollectAsync(new ReactionCollectRequest(m, timeout)).ConfigureAwait(false);
return collection;
}
///
/// Waits for specific event args to be received. Make sure the appropriate are registered, if needed.
///
///
/// The predicate.
/// Override timeout period.
public async Task> WaitForEventArgsAsync(Func predicate, TimeSpan? timeoutOverride = null) where T : AsyncEventArgs
{
var timeout = timeoutOverride ?? this.Config.Timeout;
using var waiter = new EventWaiter(this.Client);
var res = await waiter.WaitForMatchAsync(new MatchRequest(predicate, timeout)).ConfigureAwait(false);
return new InteractivityResult(res == null, res);
}
///
/// Collects the event arguments.
///
/// The predicate.
/// Override timeout period.
public async Task> CollectEventArgsAsync(Func predicate, TimeSpan? timeoutOverride = null) where T : AsyncEventArgs
{
var timeout = timeoutOverride ?? this.Config.Timeout;
using var waiter = new EventWaiter(this.Client);
var res = await waiter.CollectMatchesAsync(new CollectRequest(predicate, timeout)).ConfigureAwait(false);
return res;
}
///
/// Sends a paginated message with buttons.
///
/// The channel to send it on.
/// User to give control.
/// The pages.
/// Pagination buttons (pass null to use buttons defined in ).
/// Pagination behaviour.
/// Deletion behaviour.
/// A custom cancellation token that can be cancelled at any point.
public async Task SendPaginatedMessageAsync(
DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons,
PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default)
{
var bhv = behaviour ?? this.Config.PaginationBehaviour;
var del = deletion ?? this.Config.ButtonBehavior;
var bts = buttons ?? this.Config.PaginationButtons;
bts = new PaginationButtons(bts);
if (bhv is PaginationBehaviour.Ignore)
{
bts.SkipLeft.Disable();
bts.Left.Disable();
}
var builder = new DiscordMessageBuilder()
.WithContent(pages.First().Content)
.WithEmbed(pages.First().Embed)
.AddComponents(bts.ButtonArray);
var message = await builder.SendAsync(channel).ConfigureAwait(false);
var req = new ButtonPaginationRequest(message, user, bhv, del, bts, pages, token == default ? this.GetCancellationToken() : token);
await this._compPaginator.DoPaginationAsync(req).ConfigureAwait(false);
}
///
/// Sends a paginated message with buttons.
///
/// The channel to send it on.
/// User to give control.
/// The pages.
/// Pagination buttons (pass null to use buttons defined in ).
/// Pagination behaviour.
/// Deletion behaviour.
/// Override timeout period.
public Task SendPaginatedMessageAsync(
DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, TimeSpan? timeoutOverride,
PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default)
=> this.SendPaginatedMessageAsync(channel, user, pages, buttons, behaviour, deletion, this.GetCancellationToken(timeoutOverride));
///
/// Sends the paginated message.
///
/// The channel.
/// The user.
/// The pages.
/// The behaviour.
/// The deletion.
/// The token.
/// A Task.
public Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default)
=> this.SendPaginatedMessageAsync(channel, user, pages, default, behaviour, deletion, token);
///
/// Sends the paginated message.
///
/// The channel.
/// The user.
/// The pages.
/// Override timeout period.
/// The behaviour.
/// The deletion.
/// A Task.
public Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, TimeSpan? timeoutOverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default)
=> this.SendPaginatedMessageAsync(channel, user, pages, timeoutOverride, behaviour, deletion);
///
/// Sends a paginated message.
/// For this Event you need the intent specified in
///
/// Channel to send paginated message in.
/// User to give control.
/// Pages.
/// Pagination emojis.
/// Pagination behaviour (when hitting max and min indices).
/// Deletion behaviour.
/// Override timeout period.
public async Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationEmojis emojis,
PaginationBehaviour? behaviour = default, PaginationDeletion? deletion = default, TimeSpan? timeoutOverride = null)
{
var builder = new DiscordMessageBuilder()
.WithContent(pages.First().Content)
.WithEmbed(pages.First().Embed);
var m = await builder.SendAsync(channel).ConfigureAwait(false);
var timeout = timeoutOverride ?? this.Config.Timeout;
var bhv = behaviour ?? this.Config.PaginationBehaviour;
var del = deletion ?? this.Config.PaginationDeletion;
var ems = emojis ?? this.Config.PaginationEmojis;
var pRequest = new PaginationRequest(m, user, bhv, del, ems, timeout, pages);
await this._paginator.DoPaginationAsync(pRequest).ConfigureAwait(false);
}
///
/// Sends a paginated message in response to an interaction.
///
/// Pass the interaction directly. Interactivity will ACK it.
///
///
/// The interaction to create a response to.
/// Whether the interaction was deferred.
/// Whether the response should be ephemeral.
/// The user to listen for button presses from.
/// The pages to paginate.
/// Optional: custom buttons.
/// Pagination behaviour.
/// Deletion behaviour.
/// A custom cancellation token that can be cancelled at any point.
public async Task SendPaginatedResponseAsync(DiscordInteraction interaction, bool deferred, bool ephemeral, DiscordUser user, IEnumerable pages, PaginationButtons buttons = null, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default)
{
var bhv = behaviour ?? this.Config.PaginationBehaviour;
var del = deletion ?? this.Config.ButtonBehavior;
var bts = buttons ?? this.Config.PaginationButtons;
bts = new PaginationButtons(bts);
if (bhv is PaginationBehaviour.Ignore)
{
bts.SkipLeft.Disable();
bts.Left.Disable();
}
DiscordMessage message;
if (deferred)
{
var builder = new DiscordWebhookBuilder()
.WithContent(pages.First().Content)
.AddEmbed(pages.First().Embed)
.AddComponents(bts.ButtonArray);
message = await interaction.EditOriginalResponseAsync(builder).ConfigureAwait(false);
}
else
{
var builder = new DiscordInteractionResponseBuilder()
.WithContent(pages.First().Content)
.AddEmbed(pages.First().Embed)
.AsEphemeral(ephemeral)
.AddComponents(bts.ButtonArray);
await interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder).ConfigureAwait(false);
message = await interaction.GetOriginalResponseAsync().ConfigureAwait(false);
}
var req = new InteractionPaginationRequest(interaction, message, user, bhv, del, bts, pages, token);
await this._compPaginator.DoPaginationAsync(req).ConfigureAwait(false);
}
///
/// Waits for a custom pagination request to finish.
/// This does NOT handle removing emojis after finishing for you.
///
///
///
public async Task WaitForCustomPaginationAsync(IPaginationRequest request) => await this._paginator.DoPaginationAsync(request).ConfigureAwait(false);
///
/// Waits for custom button-based pagination request to finish.
///
/// This does not invoke .
///
/// The request to wait for.
public async Task WaitForCustomComponentPaginationAsync(IPaginationRequest request) => await this._compPaginator.DoPaginationAsync(request).ConfigureAwait(false);
///
/// Generates pages from a string, and puts them in message content.
///
/// Input string.
/// How to split input string.
///
public IEnumerable GeneratePagesInContent(string input, SplitType splitType = SplitType.Character)
{
if (string.IsNullOrEmpty(input))
throw new ArgumentException("You must provide a string that is not null or empty!");
var result = new List();
List split;
switch (splitType)
{
default:
case SplitType.Character:
split = this.SplitString(input, 500).ToList();
break;
case SplitType.Line:
var subsplit = input.Split('\n');
split = new List();
var s = "";
for (var i = 0; i < subsplit.Length; i++)
{
s += subsplit[i];
if (i >= 15 && i % 15 == 0)
{
split.Add(s);
s = "";
}
}
if (split.All(x => x != s))
split.Add(s);
break;
}
var page = 1;
foreach (var s in split)
{
result.Add(new Page($"Page {page}:\n{s}"));
page++;
}
return result;
}
///
/// Generates pages from a string, and puts them in message embeds.
///
/// Input string.
/// How to split input string.
/// Base embed for output embeds.
///
public IEnumerable GeneratePagesInEmbed(string input, SplitType splitType = SplitType.Character, DiscordEmbedBuilder embedBase = null)
{
if (string.IsNullOrEmpty(input))
throw new ArgumentException("You must provide a string that is not null or empty!");
var embed = embedBase ?? new DiscordEmbedBuilder();
var result = new List();
List split;
switch (splitType)
{
default:
case SplitType.Character:
split = this.SplitString(input, 500).ToList();
break;
case SplitType.Line:
var subsplit = input.Split('\n');
split = new List();
var s = "";
for (var i = 0; i < subsplit.Length; i++)
{
s += $"{subsplit[i]}\n";
if (i % 15 == 0 && i != 0)
{
split.Add(s);
s = "";
}
}
if (!split.Any(x => x == s))
split.Add(s);
break;
}
var page = 1;
foreach (var s in split)
{
result.Add(new Page("", new DiscordEmbedBuilder(embed).WithDescription(s).WithFooter($"Page {page}/{split.Count}")));
page++;
}
return result;
}
///
/// Splits the string.
///
/// The string.
/// The chunk size.
private List SplitString(string str, int chunkSize)
{
var res = new List();
var len = str.Length;
var i = 0;
while (i < len)
{
var size = Math.Min(len - i, chunkSize);
res.Add(str.Substring(i, size));
i += size;
}
return res;
}
///
/// Gets the cancellation token.
///
/// The timeout.
private CancellationToken GetCancellationToken(TimeSpan? timeout = null) => new CancellationTokenSource(timeout ?? this.Config.Timeout).Token;
///
/// Handles an invalid interaction.
///
/// The interaction.
private async Task HandleInvalidInteraction(DiscordInteraction interaction)
{
var at = this.Config.ResponseBehavior switch
{
InteractionResponseBehavior.Ack => interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate),
InteractionResponseBehavior.Respond => interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder { Content = this.Config.ResponseMessage, IsEphemeral = true}),
InteractionResponseBehavior.Ignore => Task.CompletedTask,
_ => throw new ArgumentException("Unknown enum value.")
};
await at;
}
}
diff --git a/DisCatSharp/Entities/Channel/DiscordChannel.cs b/DisCatSharp/Entities/Channel/DiscordChannel.cs
index 20938c4a1..fb29b6b21 100644
--- a/DisCatSharp/Entities/Channel/DiscordChannel.cs
+++ b/DisCatSharp/Entities/Channel/DiscordChannel.cs
@@ -1,1464 +1,1464 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using DisCatSharp.Enums;
using DisCatSharp.Exceptions;
using DisCatSharp.Net.Abstractions;
using DisCatSharp.Net.Models;
using Newtonsoft.Json;
namespace DisCatSharp.Entities;
///
/// Represents a discord channel.
///
public class DiscordChannel : SnowflakeObject, IEquatable
{
///
/// Gets ID of the guild to which this channel belongs.
///
[JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong? GuildId { get; internal set; }
///
/// Gets ID of the category that contains this channel.
///
[JsonProperty("parent_id", NullValueHandling = NullValueHandling.Include)]
public ulong? ParentId { get; internal set; }
///
/// Gets the category that contains this channel.
///
[JsonIgnore]
public DiscordChannel Parent
=> this.ParentId.HasValue ? this.Guild.GetChannel(this.ParentId.Value) : null;
///
/// Gets the name of this channel.
///
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
public string Name { get; internal set; }
///
/// Gets the type of this channel.
///
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
public ChannelType Type { get; internal set; }
///
/// Gets the template for new posts in this channel.
/// Applicable if forum channel.
///
[JsonProperty("template", NullValueHandling = NullValueHandling.Ignore)]
public string Template { get; internal set; }
///
/// Gets the position of this channel.
///
[JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)]
public int Position { get; internal set; }
///
/// Gets the flags of this channel.
///
[JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)]
public ChannelFlags Flags { get; internal set; }
///
/// Gets the maximum available position to move the channel to.
/// This can contain outdated information.
///
public int GetMaxPosition()
{
var channels = this.Guild.Channels.Values;
return this.ParentId != null
? this.Type == ChannelType.Text || this.Type == ChannelType.News
? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News)).OrderBy(xc => xc.Position).Last().Position
: this.Type == ChannelType.Voice || this.Type == ChannelType.Stage
? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage)).OrderBy(xc => xc.Position).Last().Position
: channels.Where(xc => xc.ParentId == this.ParentId && xc.Type == this.Type).OrderBy(xc => xc.Position).Last().Position
: channels.Where(xc => xc.ParentId == null && xc.Type == this.Type).OrderBy(xc => xc.Position).Last().Position;
}
///
/// Gets the minimum available position to move the channel to.
///
public int GetMinPosition()
{
var channels = this.Guild.Channels.Values;
return this.ParentId != null
? this.Type == ChannelType.Text || this.Type == ChannelType.News
? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News)).OrderBy(xc => xc.Position).First().Position
: this.Type == ChannelType.Voice || this.Type == ChannelType.Stage
? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage)).OrderBy(xc => xc.Position).First().Position
: channels.Where(xc => xc.ParentId == this.ParentId && xc.Type == this.Type).OrderBy(xc => xc.Position).First().Position
: channels.Where(xc => xc.ParentId == null && xc.Type == this.Type).OrderBy(xc => xc.Position).First().Position;
}
///
/// Gets whether this channel is a DM channel.
///
[JsonIgnore]
public bool IsPrivate
=> this.Type is ChannelType.Private or ChannelType.Group;
///
/// Gets whether this channel is a channel category.
///
[JsonIgnore]
public bool IsCategory
=> this.Type == ChannelType.Category;
///
/// Gets whether this channel is a stage channel.
///
[JsonIgnore]
public bool IsStage
=> this.Type == ChannelType.Stage;
///
/// Gets the guild to which this channel belongs.
///
[JsonIgnore]
public DiscordGuild Guild
=> this.GuildId.HasValue && this.Discord.Guilds.TryGetValue(this.GuildId.Value, out var guild) ? guild : null;
///
/// Gets a collection of permission overwrites for this channel.
///
[JsonIgnore]
public IReadOnlyList PermissionOverwrites
=> this._permissionOverwritesLazy.Value;
[JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)]
internal List PermissionOverwritesInternal = new();
[JsonIgnore]
private readonly Lazy> _permissionOverwritesLazy;
///
/// Gets the channel's topic. This is applicable to text channels only.
///
[JsonProperty("topic", NullValueHandling = NullValueHandling.Ignore)]
public string Topic { get; internal set; }
///
/// Gets the ID of the last message sent in this channel. This is applicable to text channels only.
///
[JsonProperty("last_message_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong? LastMessageId { get; internal set; }
///
/// Gets this channel's bitrate. This is applicable to voice channels only.
///
[JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)]
public int? Bitrate { get; internal set; }
///
/// Gets this channel's user limit. This is applicable to voice channels only.
///
[JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)]
public int? UserLimit { get; internal set; }
///
/// Gets the slow mode delay configured for this channel.
/// All bots, as well as users with or permissions in the channel are exempt from slow mode.
///
[JsonProperty("rate_limit_per_user", NullValueHandling = NullValueHandling.Ignore)]
public int? PerUserRateLimit { get; internal set; }
///
/// Gets the slow mode delay configured for this channel for post creations.
/// All bots, as well as users with or permissions in the channel are exempt from slow mode.
///
[JsonProperty("default_thread_rate_limit_per_user", NullValueHandling = NullValueHandling.Ignore)]
public int? PostCreateUserRateLimit { get; internal set; }
///
/// Gets this channel's video quality mode. This is applicable to voice channels only.
///
[JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)]
public VideoQualityMode? QualityMode { get; internal set; }
///
/// List of available tags for forum posts.
///
[JsonIgnore]
public IReadOnlyList AvailableTags => this.InternalAvailableTags;
///
/// List of available tags for forum posts.
///
[JsonProperty("available_tags", NullValueHandling = NullValueHandling.Ignore)]
internal List InternalAvailableTags { get; set; } = new();
///
/// List of available tags for forum posts.
///
[JsonProperty("default_reaction_emoji", NullValueHandling = NullValueHandling.Ignore)]
public ForumReactionEmoji DefaultReactionEmoji { get; internal set; }
[JsonProperty("default_sort_order", NullValueHandling = NullValueHandling.Include)]
public ForumPostSortOrder? DefaultSortOrder { get; internal set; }
///
/// Gets when the last pinned message was pinned.
///
[JsonIgnore]
public DateTimeOffset? LastPinTimestamp
=> !string.IsNullOrWhiteSpace(this.LastPinTimestampRaw) && DateTimeOffset.TryParse(this.LastPinTimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ?
dto : null;
///
/// Gets when the last pinned message was pinned as raw string.
///
[JsonProperty("last_pin_timestamp", NullValueHandling = NullValueHandling.Ignore)]
internal string LastPinTimestampRaw { get; set; }
///
/// Gets this channel's default duration for newly created threads, in minutes, to automatically archive the thread after recent activity.
///
[JsonProperty("default_auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)]
public ThreadAutoArchiveDuration? DefaultAutoArchiveDuration { get; internal set; }
///
/// Gets this channel's mention string.
///
[JsonIgnore]
public string Mention
=> Formatter.Mention(this);
///
/// Gets this channel's children. This applies only to channel categories.
///
[JsonIgnore]
public IReadOnlyList Children =>
!this.IsCategory
? throw new ArgumentException("Only channel categories contain children.")
: this.Guild.ChannelsInternal.Values.Where(e => e.ParentId == this.Id).ToList();
///
/// Gets the list of members currently in the channel (if voice channel), or members who can see the channel (otherwise).
///
[JsonIgnore]
public virtual IReadOnlyList Users =>
this.Guild == null
? throw new InvalidOperationException("Cannot query users outside of guild channels.")
: this.IsVoiceJoinable()
? this.Guild.Members.Values.Where(x => x.VoiceState?.ChannelId == this.Id).ToList()
: this.Guild.Members.Values.Where(x => (this.PermissionsFor(x) & Permissions.AccessChannels) == Permissions.AccessChannels).ToList();
///
/// Gets whether this channel is an NSFW channel.
///
[JsonProperty("nsfw")]
public bool IsNsfw { get; internal set; }
///
/// Gets this channel's region id (if voice channel).
///
[JsonProperty("rtc_region", NullValueHandling = NullValueHandling.Ignore)]
internal string RtcRegionId { get; set; }
///
/// Gets this channel's region override (if voice channel).
///
[JsonIgnore]
public DiscordVoiceRegion RtcRegion
=> this.RtcRegionId != null ? this.Discord.VoiceRegions[this.RtcRegionId] : null;
///
/// Only sent on the resolved channels of interaction responses for application commands.
/// Gets the permissions of the user in this channel who invoked the command.
///
[JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)]
public Permissions? UserPermissions { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal DiscordChannel()
{
this._permissionOverwritesLazy = new Lazy>(() => new ReadOnlyCollection(this.PermissionOverwritesInternal));
}
#region Methods
///
/// Sends a message to this channel.
///
/// Content of the message to send.
/// The sent message.
/// Thrown when the client does not have the permission if TTS is true and if TTS is true.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task SendMessageAsync(string content) =>
!this.IsWritable()
? throw new ArgumentException("Cannot send a text message to a non-text channel.")
: this.Discord.ApiClient.CreateMessageAsync(this.Id, content, null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false);
///
/// Sends a message to this channel.
///
/// Embed to attach to the message.
/// The sent message.
/// Thrown when the client does not have the permission and if TTS is true.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task SendMessageAsync(DiscordEmbed embed) =>
!this.IsWritable()
? throw new ArgumentException("Cannot send a text message to a non-text channel.")
: this.Discord.ApiClient.CreateMessageAsync(this.Id, null, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false);
///
/// Sends a message to this channel.
///
/// Embed to attach to the message.
/// Content of the message to send.
/// The sent message.
/// Thrown when the client does not have the permission if TTS is true and if TTS is true.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task SendMessageAsync(string content, DiscordEmbed embed) =>
!this.IsWritable()
? throw new ArgumentException("Cannot send a text message to a non-text channel.")
: this.Discord.ApiClient.CreateMessageAsync(this.Id, content, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false);
///
/// Sends a message to this channel.
///
/// The builder with all the items to send.
/// The sent message.
/// Thrown when the client does not have the permission TTS is true and if TTS is true.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task SendMessageAsync(DiscordMessageBuilder builder)
=> this.Discord.ApiClient.CreateMessageAsync(this.Id, builder);
///
/// Sends a message to this channel.
///
/// The builder with all the items to send.
/// The sent message.
/// Thrown when the client does not have the permission TTS is true and if TTS is true.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task SendMessageAsync(Action action)
{
var builder = new DiscordMessageBuilder();
action(builder);
return !this.IsWritable()
? throw new ArgumentException("Cannot send a text message to a non-text channel.")
: this.Discord.ApiClient.CreateMessageAsync(this.Id, builder);
}
///
/// Deletes a guild channel
///
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteAsync(string reason = null)
=> this.Discord.ApiClient.DeleteChannelAsync(this.Id, reason);
///
/// Clones this channel. This operation will create a channel with identical settings to this one. Note that this will not copy messages.
///
/// Reason for audit logs.
/// Newly-created channel.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task CloneAsync(string reason = null)
{
if (this.Guild == null)
throw new InvalidOperationException("Non-guild channels cannot be cloned.");
var ovrs = new List();
foreach (var ovr in this.PermissionOverwritesInternal)
ovrs.Add(await new DiscordOverwriteBuilder().FromAsync(ovr).ConfigureAwait(false));
// TODO: Add forum tags option missing?
var bitrate = this.Bitrate;
var userLimit = this.UserLimit;
Optional perUserRateLimit = this.PerUserRateLimit;
if (!this.IsVoiceJoinable())
{
bitrate = null;
userLimit = null;
}
if (this.Type == ChannelType.Stage)
{
userLimit = null;
}
if (!this.IsWritable())
{
perUserRateLimit = Optional.None;
}
return await this.Guild.CreateChannelAsync(this.Name, this.Type, this.Parent, this.Topic, bitrate, userLimit, ovrs, this.IsNsfw, perUserRateLimit, this.QualityMode, this.DefaultAutoArchiveDuration, this.Flags, reason).ConfigureAwait(false);
}
///
/// Gets a specific message.
///
/// The id of the message
/// Whether to bypass the cache. Defaults to false.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task GetMessageAsync(ulong id, bool fetch = false) =>
this.Discord.Configuration.MessageCacheSize > 0
&& !fetch
&& this.Discord is DiscordClient dc
&& dc.MessageCache != null
&& dc.MessageCache.TryGet(xm => xm.Id == id && xm.ChannelId == this.Id, out var msg)
? msg
: await this.Discord.ApiClient.GetMessageAsync(this.Id, id).ConfigureAwait(false);
///
/// Tries to get a specific message.
///
/// The id of the message
/// Whether to bypass the cache. Defaults to true.
/// Thrown when the client does not have the permission.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task TryGetMessageAsync(ulong id, bool fetch = true)
{
try
{
return await this.GetMessageAsync(id, fetch).ConfigureAwait(false);
}
catch (NotFoundException)
{
return null;
}
}
///
/// Modifies the current channel.
///
/// Action to perform on this channel
/// Thrown when the client does not have the .
/// Thrown when the client does not have the correct for modifying the .
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifyAsync(Action action)
{
if (this.Type == ChannelType.Forum)
throw new NotSupportedException("Cannot execute this request on a forum channel.");
var mdl = new ChannelEditModel();
action(mdl);
if (mdl.DefaultAutoArchiveDuration.HasValue)
if (!Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, mdl.DefaultAutoArchiveDuration.Value))
throw new NotSupportedException($"Cannot modify DefaultAutoArchiveDuration. Guild needs boost tier {(mdl.DefaultAutoArchiveDuration.Value == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}.");
return this.Discord.ApiClient.ModifyChannelAsync(this.Id, mdl.Name, mdl.Position, mdl.Topic, mdl.Nsfw,
mdl.Parent.Map(p => p?.Id), mdl.Bitrate, mdl.UserLimit, mdl.PerUserRateLimit, mdl.RtcRegion.Map(r => r?.Id),
mdl.QualityMode, mdl.DefaultAutoArchiveDuration, mdl.Type, mdl.PermissionOverwrites, mdl.Flags, mdl.AuditLogReason);
}
///
/// Modifies the current forum channel.
///
/// Action to perform on this channel
/// Thrown when the client does not have the .
/// Thrown when the client does not have the correct for modifying the .
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifyForumAsync(Action action)
{
if (this.Type != ChannelType.Forum)
throw new NotSupportedException("Cannot execute this request on a non-forum channel.");
var mdl = new ForumChannelEditModel();
action(mdl);
if (mdl.DefaultAutoArchiveDuration.HasValue && mdl.DefaultAutoArchiveDuration.Value.HasValue)
if (!Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, mdl.DefaultAutoArchiveDuration.Value.Value))
throw new NotSupportedException($"Cannot modify DefaultAutoArchiveDuration. Guild needs boost tier {(mdl.DefaultAutoArchiveDuration.Value == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}.");
return this.Discord.ApiClient.ModifyForumChannelAsync(this.Id, mdl.Name, mdl.Position, mdl.Topic, mdl.Template, mdl.Nsfw,
mdl.Parent.Map(p => p?.Id), mdl.AvailableTags, mdl.DefaultReactionEmoji, mdl.PerUserRateLimit, mdl.PostCreateUserRateLimit,
mdl.DefaultSortOrder, mdl.DefaultAutoArchiveDuration, mdl.PermissionOverwrites, mdl.Flags, mdl.AuditLogReason);
}
///
/// Updates the channel position when it doesn't have a category.
///
/// Use for moving to other categories.
/// Use to move out of a category.
/// Use for moving within a category.
///
/// Position the channel should be moved to.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifyPositionAsync(int position, string reason = null)
{
if (this.Guild == null)
throw new ArgumentException("Cannot modify order of non-guild channels.");
if (!this.IsMovable())
throw new NotSupportedException("You can't move this type of channel in categories.");
if (this.ParentId != null)
throw new ArgumentException("Cannot modify order of channels within a category. Use ModifyPositionInCategoryAsync instead.");
var pmds = this.Guild.ChannelsInternal.Values.Where(xc => xc.Type == this.Type).OrderBy(xc => xc.Position)
.Select(x => new RestGuildChannelReorderPayload
{
ChannelId = x.Id,
Position = x.Id == this.Id ? position : x.Position >= position ? x.Position + 1 : x.Position
});
return this.Discord.ApiClient.ModifyGuildChannelPositionAsync(this.Guild.Id, pmds, reason);
}
///
/// Updates the channel position within it's own category.
///
/// Use for moving to other categories.
/// Use to move out of a category.
/// Use to move channels outside a category.
///
/// The position.
/// The reason.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
/// Thrown when is out of range.
/// Thrown when function is called on a channel without a parent channel.
public async Task ModifyPositionInCategoryAsync(int position, string reason = null)
{
if (!this.IsMovableInParent())
throw new NotSupportedException("You can't move this type of channel in categories.");
var isUp = position > this.Position;
var channels = await this.InternalRefreshChannelsAsync();
var chns = this.ParentId != null
? this.Type == ChannelType.Text || this.Type == ChannelType.News
? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News))
: this.Type == ChannelType.Voice || this.Type == ChannelType.Stage
? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage))
: channels.Where(xc => xc.ParentId == this.ParentId && xc.Type == this.Type)
: this.Type == ChannelType.Text || this.Type == ChannelType.News
? channels.Where(xc => xc.ParentId == null && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News))
: this.Type == ChannelType.Voice || this.Type == ChannelType.Stage
? channels.Where(xc => xc.ParentId == null && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage))
: channels.Where(xc => xc.ParentId == null && xc.Type == this.Type);
var ochns = chns.OrderBy(xc => xc.Position).ToArray();
var min = ochns.First().Position;
var max = ochns.Last().Position;
if (position > max || position < min)
throw new IndexOutOfRangeException($"Position is not in range. {position} is {(position > max ? "greater then the maximal" : "lower then the minimal")} position.");
var pmds = ochns.Select(x =>
new RestGuildChannelReorderPayload
{
ChannelId = x.Id,
Position = x.Id == this.Id
? position
: isUp
? x.Position <= position && x.Position > this.Position ? x.Position - 1 : x.Position
: x.Position >= position && x.Position < this.Position ? x.Position + 1 : x.Position
}
);
await this.Discord.ApiClient.ModifyGuildChannelPositionAsync(this.Guild.Id, pmds, reason).ConfigureAwait(false);
}
///
/// Internally refreshes the channel list.
///
private async Task> InternalRefreshChannelsAsync()
{
await this.RefreshPositionsAsync();
return this.Guild.Channels.Values.ToList().AsReadOnly();
}
internal void Initialize(BaseDiscordClient client)
{
this.Discord = client;
foreach (var xo in this.PermissionOverwritesInternal)
{
xo.Discord = this.Discord;
xo.ChannelId = this.Id;
}
if (this.InternalAvailableTags != null)
{
foreach (var xo in this.InternalAvailableTags)
{
xo.Discord = this.Discord;
xo.ChannelId = this.Id;
xo.Channel = this;
}
}
}
///
/// Refreshes the positions.
///
public async Task RefreshPositionsAsync()
{
var channels = await this.Discord.ApiClient.GetGuildChannelsAsync(this.Guild.Id);
this.Guild.ChannelsInternal.Clear();
foreach (var channel in channels.ToList())
{
channel.Initialize(this.Discord);
this.Guild.ChannelsInternal[channel.Id] = channel;
}
}
///
/// Updates the channel position within it's own category.
/// Valid modes: '+' or 'down' to move a channel down | '-' or 'up' to move a channel up.
///
/// Use for moving to other categories.
/// Use to move out of a category.
/// Use to move channels outside a category.
///
/// The mode. Valid: '+' or 'down' to move a channel down | '-' or 'up' to move a channel up
/// The position.
/// The reason.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
/// Thrown when is out of range.
/// Thrown when function is called on a channel without a parent channel, a wrong mode is given or given position is zero.
public Task ModifyPositionInCategorySmartAsync(string mode, int position, string reason = null)
{
if (!this.IsMovableInParent())
throw new NotSupportedException("You can't move this type of channel in categories.");
if (mode != "+" && mode != "-" && mode != "down" && mode != "up")
throw new ArgumentException("Error with the selected mode: Valid is '+' or 'down' to move a channel down and '-' or 'up' to move a channel up");
var positive = mode == "+" || mode == "positive" || mode == "down";
var negative = mode == "-" || mode == "negative" || mode == "up";
return positive
? position < this.GetMaxPosition()
? this.ModifyPositionInCategoryAsync(this.Position + position, reason)
: throw new IndexOutOfRangeException($"Position is not in range of category.")
: negative
? position > this.GetMinPosition()
? this.ModifyPositionInCategoryAsync(this.Position - position, reason)
: throw new IndexOutOfRangeException($"Position is not in range of category.")
: throw new ArgumentException("You can only modify with +X or -X. 0 is not valid.");
}
///
/// Updates the channel parent, moving the channel to the bottom of the new category.
///
/// New parent for channel. Use to remove from parent.
/// Sync permissions with parent. Defaults to null.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifyParentAsync(DiscordChannel newParent, bool? lockPermissions = null, string reason = null)
{
if (this.Guild == null)
throw new ArgumentException("Cannot modify parent of non-guild channels.");
if (!this.IsMovableInParent())
throw new NotSupportedException("You can't move this type of channel in categories.");
if (newParent.Type is not ChannelType.Category)
throw new ArgumentException("Only category type channels can be parents.");
var position = this.Guild.ChannelsInternal.Values.Where(xc => xc.Type == this.Type && xc.ParentId == newParent.Id) // gets list same type channels in parent
.Select(xc => xc.Position).DefaultIfEmpty(-1).Max() + 1; // returns highest position of list +1, default val: 0
var pmds = this.Guild.ChannelsInternal.Values.Where(xc => xc.Type == this.Type)
.OrderBy(xc => xc.Position)
.Select(x =>
{
var pmd = new RestGuildChannelNewParentPayload
{
ChannelId = x.Id,
Position = x.Position >= position ? x.Position + 1 : x.Position,
};
if (x.Id == this.Id)
{
pmd.Position = position;
pmd.ParentId = newParent?.Id;
pmd.LockPermissions = lockPermissions;
}
return pmd;
});
return this.Discord.ApiClient.ModifyGuildChannelParentAsync(this.Guild.Id, pmds, reason);
}
///
/// Moves the channel out of a category.
///
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task RemoveParentAsync(string reason = null)
{
if (this.Guild == null)
throw new ArgumentException("Cannot modify parent of non-guild channels.");
if (!this.IsMovableInParent())
throw new NotSupportedException("You can't move this type of channel in categories.");
var pmds = this.Guild.ChannelsInternal.Values.Where(xc => xc.Type == this.Type)
.OrderBy(xc => xc.Position)
.Select(x =>
{
var pmd = new RestGuildChannelNoParentPayload { ChannelId = x.Id };
if (x.Id == this.Id)
{
pmd.Position = 1;
pmd.ParentId = null;
}
else
{
pmd.Position = x.Position < this.Position ? x.Position + 1 : x.Position;
}
return pmd;
});
return this.Discord.ApiClient.DetachGuildChannelParentAsync(this.Guild.Id, pmds, reason);
}
///
/// Returns a list of messages before a certain message.
/// The amount of messages to fetch.
/// Message to fetch before from.
///
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task> GetMessagesBeforeAsync(ulong before, int limit = 100)
=> this.GetMessagesInternalAsync(limit, before, null, null);
///
/// Returns a list of messages after a certain message.
/// The amount of messages to fetch.
/// Message to fetch after from.
///
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task> GetMessagesAfterAsync(ulong after, int limit = 100)
=> this.GetMessagesInternalAsync(limit, null, after, null);
///
/// Returns a list of messages around a certain message.
/// The amount of messages to fetch.
/// Message to fetch around from.
///
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task> GetMessagesAroundAsync(ulong around, int limit = 100)
=> this.GetMessagesInternalAsync(limit, null, null, around);
///
/// Returns a list of messages from the last message in the channel.
/// The amount of messages to fetch.
///
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task> GetMessagesAsync(int limit = 100) =>
this.GetMessagesInternalAsync(limit, null, null, null);
///
/// Returns a list of messages
///
/// How many messages should be returned.
/// Get messages before snowflake.
/// Get messages after snowflake.
/// Get messages around snowflake.
private async Task> GetMessagesInternalAsync(int limit = 100, ulong? before = null, ulong? after = null, ulong? around = null)
{
if (!this.IsWritable())
throw new ArgumentException("Cannot get the messages of a non-text channel.");
if (limit < 0)
throw new ArgumentException("Cannot get a negative number of messages.");
if (limit == 0)
return Array.Empty();
//return this.Discord.ApiClient.GetChannelMessagesAsync(this.Id, limit, before, after, around);
if (limit > 100 && around != null)
throw new InvalidOperationException("Cannot get more than 100 messages around the specified ID.");
var msgs = new List(limit);
var remaining = limit;
ulong? last = null;
var isAfter = after != null;
int lastCount;
do
{
var fetchSize = remaining > 100 ? 100 : remaining;
var fetch = await this.Discord.ApiClient.GetChannelMessagesAsync(this.Id, fetchSize, !isAfter ? last ?? before : null, isAfter ? last ?? after : null, around).ConfigureAwait(false);
lastCount = fetch.Count;
remaining -= lastCount;
if (!isAfter)
{
msgs.AddRange(fetch);
last = fetch.LastOrDefault()?.Id;
}
else
{
msgs.InsertRange(0, fetch);
last = fetch.FirstOrDefault()?.Id;
}
}
while (remaining > 0 && lastCount > 0);
return new ReadOnlyCollection(msgs);
}
///
/// Deletes multiple messages if they are less than 14 days old. If they are older, none of the messages will be deleted and you will receive a error.
///
/// A collection of messages to delete.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task DeleteMessagesAsync(IEnumerable messages, string reason = null)
{
// don't enumerate more than once
var msgs = messages.Where(x => x.Channel.Id == this.Id).Select(x => x.Id).ToArray();
if (messages == null || !msgs.Any())
throw new ArgumentException("You need to specify at least one message to delete.");
if (msgs.Length < 2)
{
await this.Discord.ApiClient.DeleteMessageAsync(this.Id, msgs.Single(), reason).ConfigureAwait(false);
return;
}
for (var i = 0; i < msgs.Length; i += 100)
await this.Discord.ApiClient.DeleteMessagesAsync(this.Id, msgs.Skip(i).Take(100), reason).ConfigureAwait(false);
}
///
/// Deletes a message
///
/// The message to be deleted.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteMessageAsync(DiscordMessage message, string reason = null)
=> this.Discord.ApiClient.DeleteMessageAsync(this.Id, message.Id, reason);
///
/// Returns a list of invite objects
///
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task> GetInvitesAsync() =>
this.Guild == null
? throw new ArgumentException("Cannot get the invites of a channel that does not belong to a guild.")
: this.Discord.ApiClient.GetChannelInvitesAsync(this.Id);
///
/// Create a new invite object
///
/// Duration of invite in seconds before expiry, or 0 for never. Defaults to 86400.
/// Max number of uses or 0 for unlimited. Defaults to 0
/// Whether this invite should be temporary. Defaults to false.
/// Whether this invite should be unique. Defaults to false.
/// The target type. Defaults to null.
/// The target activity ID. Defaults to null.
/// The target user id. Defaults to null.
/// The audit log reason.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task CreateInviteAsync(int maxAge = 86400, int maxUses = 0, bool temporary = false, bool unique = false, TargetType? targetType = null, ulong? targetApplicationId = null, ulong? targetUser = null, string reason = null)
=> this.Discord.ApiClient.CreateChannelInviteAsync(this.Id, maxAge, maxUses, targetType, targetApplicationId, targetUser, temporary, unique, reason);
#region Stage
///
/// Opens a stage.
///
/// Topic of the stage.
/// Whether @everyone should be notified.
/// Privacy level of the stage (Defaults to .
/// Audit log reason.
/// Stage instance
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task OpenStageAsync(string topic, bool sendStartNotification = false, StagePrivacyLevel privacyLevel = StagePrivacyLevel.GuildOnly, string reason = null)
=> await this.Discord.ApiClient.CreateStageInstanceAsync(this.Id, topic, sendStartNotification, privacyLevel, reason);
///
/// Modifies a stage topic.
///
/// New topic of the stage.
/// New privacy level of the stage.
/// Audit log reason.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task ModifyStageAsync(Optional topic, Optional privacyLevel, string reason = null)
=> await this.Discord.ApiClient.ModifyStageInstanceAsync(this.Id, topic, privacyLevel, reason);
///
/// Closes a stage.
///
/// Audit log reason.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task CloseStageAsync(string reason = null)
=> await this.Discord.ApiClient.DeleteStageInstanceAsync(this.Id, reason);
///
/// Gets a stage.
///
/// The requested stage.
/// Thrown when the client does not have the or permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task GetStageAsync()
=> await this.Discord.ApiClient.GetStageInstanceAsync(this.Id);
#endregion
#region Scheduled Events
///
/// Creates a scheduled event based on the channel type.
///
/// The name.
/// The scheduled start time.
/// The description.
/// The cover image.
/// The reason.
/// A scheduled event.
/// Thrown when the resource does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task CreateScheduledEventAsync(string name, DateTimeOffset scheduledStartTime, string description = null, Optional coverImage = default, string reason = null)
{
if (!this.IsVoiceJoinable())
throw new NotSupportedException("Cannot create a scheduled event for this type of channel. Channel type must be either voice or stage.");
var type = this.Type == ChannelType.Voice ? ScheduledEventEntityType.Voice : ScheduledEventEntityType.StageInstance;
return await this.Guild.CreateScheduledEventAsync(name, scheduledStartTime, null, this, null, description, type, coverImage, reason);
}
#endregion
#region Threads
///
/// Creates a thread.
/// Depending on whether it is created inside an or an it is either an or an .
/// Depending on whether the is set to it is either an or an (default).
///
/// The name of the thread.
/// till it gets archived. Defaults to .
/// Can be either an , or an .
/// The per user ratelimit, aka slowdown.
/// Audit log reason.
/// The created thread.
/// Thrown when the client does not have the or or if creating a private thread the permission.
/// Thrown when the guild hasn't enabled threads atm.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
/// Thrown when the cannot be modified. This happens, when the guild hasn't reached a certain boost . Or if is not enabled for guild. This happens, if the guild does not have
public async Task CreateThreadAsync(string name, ThreadAutoArchiveDuration autoArchiveDuration = ThreadAutoArchiveDuration.OneHour, ChannelType type = ChannelType.PublicThread, int? rateLimitPerUser = null, string reason = null) =>
type != ChannelType.NewsThread && type != ChannelType.PublicThread && type != ChannelType.PrivateThread
? throw new NotSupportedException("Wrong thread type given.")
: !this.IsThreadHolder()
? throw new NotSupportedException("Parent channel can't have threads.")
: type == ChannelType.PrivateThread
? Utilities.CheckThreadPrivateFeature(this.Guild)
? Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, autoArchiveDuration)
? await this.Discord.ApiClient.CreateThreadAsync(this.Id, null, name, autoArchiveDuration, type, rateLimitPerUser, isForum: false, reason: reason)
: throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(autoArchiveDuration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}.")
: throw new NotSupportedException($"Cannot create a private thread. Guild needs to be boost tier two.")
: Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, autoArchiveDuration)
? await this.Discord.ApiClient.CreateThreadAsync(this.Id, null, name, autoArchiveDuration, this.Type == ChannelType.News ? ChannelType.NewsThread : ChannelType.PublicThread, rateLimitPerUser, isForum: false, reason: reason)
: throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(autoArchiveDuration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}.");
///
/// Creates a forum post.
///
/// The name of the post.
/// The message of the post.
/// The per user ratelimit, aka slowdown.
/// The tags to add on creation.
/// Audit log reason.
/// The created thread.
/// Thrown when the client does not have the permission.
/// Thrown when the guild hasn't enabled threads atm.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task CreatePostAsync(string name, DiscordMessageBuilder builder, int? rateLimitPerUser = null, IEnumerable? tags = null, string reason = null)
- => this.Type != ChannelType.Forum ? throw new NotSupportedException("Parent channel must be forum.") : await this.Discord.ApiClient.CreateThreadAsync(this.Id, null, name, null, null, rateLimitPerUser, tags, builder, true, reason);
+ => this.Type != ChannelType.Forum ? throw new NotSupportedException("Parent channel must be forum.") : await this.Discord.ApiClient.CreateThreadAsync(this.Id, null, name, null, null, rateLimitPerUser, tags, builder, true, reason);
///
/// Gets joined archived private threads. Can contain more threads.
/// If the result's value 'HasMore' is true, you need to recall this function to get older threads.
///
/// Get threads created before this thread id.
/// Defines the limit of returned .
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task GetJoinedPrivateArchivedThreadsAsync(ulong? before, int? limit)
=> await this.Discord.ApiClient.GetJoinedPrivateArchivedThreadsAsync(this.Id, before, limit);
///
/// Gets archived public threads. Can contain more threads.
/// If the result's value 'HasMore' is true, you need to recall this function to get older threads.
///
/// Get threads created before this thread id.
/// Defines the limit of returned .
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task GetPublicArchivedThreadsAsync(ulong? before, int? limit)
=> await this.Discord.ApiClient.GetPublicArchivedThreadsAsync(this.Id, before, limit);
///
/// Gets archived private threads. Can contain more threads.
/// If the result's value 'HasMore' is true, you need to recall this function to get older threads.
///
/// Get threads created before this thread id.
/// Defines the limit of returned .
/// Thrown when the client does not have the or permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task GetPrivateArchivedThreadsAsync(ulong? before, int? limit)
=> await this.Discord.ApiClient.GetPrivateArchivedThreadsAsync(this.Id, before, limit);
///
/// Gets a forum channel tag.
///
/// The id of the tag to get.
/// Thrown when the tag does not exist.
public ForumPostTag GetForumPostTag(ulong id)
{
var tag = this.InternalAvailableTags.First(x => x.Id == id);
tag.Discord = this.Discord;
tag.ChannelId = this.Id;
tag.Channel = this;
return tag;
}
///
/// Tries to get a forum channel tag.
///
/// The id of the tag to get or null if not found.
public ForumPostTag? TryGetForumPostTag(ulong id)
{
var tag = this.InternalAvailableTags.FirstOrDefault(x => x.Id == id);
if (tag is not null)
{
tag.Discord = this.Discord;
tag.ChannelId = this.Id;
}
return tag;
}
///
/// Creates a forum channel tag.
///
/// The name of the tag.
/// The emoji of the tag. Has to be either a of the current guild or a .
/// Whether only moderators should be able to apply this tag.
/// The audit log reason.
/// Thrown when the client does not have the permission.
/// Thrown when the tag does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task CreateForumPostTagAsync(string name, DiscordEmoji emoji = null, bool moderated = false, string reason = null)
=> this.Type != ChannelType.Forum ? throw new NotSupportedException("Channel needs to be type of Forum") : await this.Discord.ApiClient.ModifyForumChannelAsync(this.Id, null, null, Optional.None, Optional.None, null, Optional.None, this.InternalAvailableTags.Append(new ForumPostTag()
{
Name = name,
EmojiId = emoji != null && emoji.Id != 0 ? emoji.Id : null,
UnicodeEmojiString = emoji?.Id == null || emoji?.Id == 0 ? emoji?.Name ?? null : null,
Moderated = moderated,
Id = null
}).ToList(), Optional.None, Optional.None, Optional.None, Optional.None, Optional.None, null, Optional.None, reason);
///
/// Deletes a forum channel tag.
///
/// The id of the tag to delete.
/// The audit log reason.
/// Thrown when the client does not have the permission.
/// Thrown when the tag does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task DeleteForumPostTag(ulong id, string reason = null)
=> this.Type != ChannelType.Forum ? throw new NotSupportedException("Channel needs to be type of Forum") : await this.Discord.ApiClient.ModifyForumChannelAsync(this.Id, null, null, Optional.None, Optional.None, null, Optional.None, this.InternalAvailableTags?.Where(x => x.Id != id)?.ToList(), Optional.None, Optional.None, Optional.None, Optional.None, Optional.None, null, Optional.None, reason);
#endregion
///
/// Adds a channel permission overwrite for specified role.
///
/// The role to have the permission added.
/// The permissions to allow.
/// The permissions to deny.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task AddOverwriteAsync(DiscordRole role, Permissions allow = Permissions.None, Permissions deny = Permissions.None, string reason = null)
=> this.Discord.ApiClient.EditChannelPermissionsAsync(this.Id, role.Id, allow, deny, "role", reason);
///
/// Adds a channel permission overwrite for specified member.
///
/// The member to have the permission added.
/// The permissions to allow.
/// The permissions to deny.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task AddOverwriteAsync(DiscordMember member, Permissions allow = Permissions.None, Permissions deny = Permissions.None, string reason = null)
=> this.Discord.ApiClient.EditChannelPermissionsAsync(this.Id, member.Id, allow, deny, "member", reason);
///
/// Deletes a channel permission overwrite for specified member.
///
/// The member to have the permission deleted.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteOverwriteAsync(DiscordMember member, string reason = null)
=> this.Discord.ApiClient.DeleteChannelPermissionAsync(this.Id, member.Id, reason);
///
/// Deletes a channel permission overwrite for specified role.
///
/// The role to have the permission deleted.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteOverwriteAsync(DiscordRole role, string reason = null)
=> this.Discord.ApiClient.DeleteChannelPermissionAsync(this.Id, role.Id, reason);
///
/// Post a typing indicator.
///
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task TriggerTypingAsync() =>
!this.IsWritable()
? throw new ArgumentException("Cannot start typing in a non-text channel.")
: this.Discord.ApiClient.TriggerTypingAsync(this.Id);
///
/// Returns all pinned messages.
///
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task> GetPinnedMessagesAsync() =>
!this.IsWritable()
? throw new ArgumentException("A non-text channel does not have pinned messages.")
: this.Discord.ApiClient.GetPinnedMessagesAsync(this.Id);
///
/// Create a new webhook.
///
/// The name of the webhook.
/// The image for the default webhook avatar.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task CreateWebhookAsync(string name, Optional avatar = default, string reason = null)
=> await this.Discord.ApiClient.CreateWebhookAsync(this.IsThread() ? this.ParentId!.Value : this.Id, name,
ImageTool.Base64FromStream(avatar), reason).ConfigureAwait(false);
///
/// Returns a list of webhooks.
///
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when Discord is unable to process the request.
public Task> GetWebhooksAsync()
=> this.Discord.ApiClient.GetChannelWebhooksAsync(this.IsThread() ? this.ParentId!.Value : this.Id);
///
/// Moves a member to this voice channel.
///
/// The member to be moved.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exists or if the Member does not exists.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task PlaceMemberAsync(DiscordMember member)
{
if (!this.IsVoiceJoinable())
throw new ArgumentException("Cannot place a member in a non-voice channel.");
await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, member.Id, default, default, default,
default, this.Id, null).ConfigureAwait(false);
}
///
/// Follows a news channel.
///
/// Channel to crosspost messages to.
/// Thrown when trying to follow a non-news channel.
/// Thrown when the current user doesn't have on the target channel.
public Task FollowAsync(DiscordChannel targetChannel) =>
this.Type != ChannelType.News
? throw new ArgumentException("Cannot follow a non-news channel.")
: this.Discord.ApiClient.FollowChannelAsync(this.Id, targetChannel.Id);
///
/// Publishes a message in a news channel to following channels.
///
/// Message to publish.
/// Thrown when the message has already been crossposted.
///
/// Thrown when the current user doesn't have and/or
///
public Task CrosspostMessageAsync(DiscordMessage message) =>
(message.Flags & MessageFlags.Crossposted) == MessageFlags.Crossposted
? throw new ArgumentException("Message is already crossposted.")
: this.Discord.ApiClient.CrosspostMessageAsync(this.Id, message.Id);
///
/// Updates the current user's suppress state in this channel, if stage channel.
///
/// Toggles the suppress state.
/// Sets the time the user requested to speak.
/// Thrown when the channel is not a stage channel.
public async Task UpdateCurrentUserVoiceStateAsync(bool? suppress, DateTimeOffset? requestToSpeakTimestamp = null)
{
if (this.Type != ChannelType.Stage)
throw new ArgumentException("Voice state can only be updated in a stage channel.");
await this.Discord.ApiClient.UpdateCurrentUserVoiceStateAsync(this.GuildId.Value, this.Id, suppress, requestToSpeakTimestamp).ConfigureAwait(false);
}
///
/// Calculates permissions for a given member.
///
/// Member to calculate permissions for.
/// Calculated permissions for a given member.
public Permissions PermissionsFor(DiscordMember mbr)
{
// user > role > everyone
// allow > deny > undefined
// =>
// user allow > user deny > role allow > role deny > everyone allow > everyone deny
if (this.IsPrivate || this.Guild == null)
return Permissions.None;
if (this.Guild.OwnerId == mbr.Id)
return PermissionMethods.FullPerms;
Permissions perms;
// assign @everyone permissions
var everyoneRole = this.Guild.EveryoneRole;
perms = everyoneRole.Permissions;
// roles that member is in
var mbRoles = mbr.Roles.Where(xr => xr.Id != everyoneRole.Id);
// assign permissions from member's roles (in order)
perms |= mbRoles.Aggregate(Permissions.None, (c, role) => c | role.Permissions);
// Administrator grants all permissions and cannot be overridden
if ((perms & Permissions.Administrator) == Permissions.Administrator)
return PermissionMethods.FullPerms;
// channel overrides for roles that member is in
var mbRoleOverrides = mbRoles
.Select(xr => this.PermissionOverwritesInternal.FirstOrDefault(xo => xo.Id == xr.Id))
.Where(xo => xo != null)
.ToList();
// assign channel permission overwrites for @everyone pseudo-role
var everyoneOverwrites = this.PermissionOverwritesInternal.FirstOrDefault(xo => xo.Id == everyoneRole.Id);
if (everyoneOverwrites != null)
{
perms &= ~everyoneOverwrites.Denied;
perms |= everyoneOverwrites.Allowed;
}
// assign channel permission overwrites for member's roles (explicit deny)
perms &= ~mbRoleOverrides.Aggregate(Permissions.None, (c, overs) => c | overs.Denied);
// assign channel permission overwrites for member's roles (explicit allow)
perms |= mbRoleOverrides.Aggregate(Permissions.None, (c, overs) => c | overs.Allowed);
// channel overrides for just this member
var mbOverrides = this.PermissionOverwritesInternal.FirstOrDefault(xo => xo.Id == mbr.Id);
if (mbOverrides == null) return perms;
// assign channel permission overwrites for just this member
perms &= ~mbOverrides.Denied;
perms |= mbOverrides.Allowed;
return perms;
}
///
/// Returns a string representation of this channel.
///
/// String representation of this channel.
public override string ToString() =>
this.Type == ChannelType.Category
? $"Channel Category {this.Name} ({this.Id})"
: this.Type == ChannelType.Text || this.Type == ChannelType.News || this.IsThread()
? $"Channel #{this.Name} ({this.Id})"
: this.IsVoiceJoinable()
? $"Channel #!{this.Name} ({this.Id})"
: !string.IsNullOrWhiteSpace(this.Name) ? $"Channel {this.Name} ({this.Id})" : $"Channel {this.Id}";
#endregion
///
/// Checks whether this is equal to another object.
///
/// Object to compare to.
/// Whether the object is equal to this .
public override bool Equals(object obj)
=> this.Equals(obj as DiscordChannel);
///
/// Checks whether this is equal to another .
///
/// to compare to.
/// Whether the is equal to this .
public bool Equals(DiscordChannel e)
=> e is not null && (ReferenceEquals(this, e) || this.Id == e.Id);
///
/// Gets the hash code for this .
///
/// The hash code for this .
public override int GetHashCode()
=> this.Id.GetHashCode();
///
/// Gets whether the two objects are equal.
///
/// First channel to compare.
/// Second channel to compare.
/// Whether the two channels are equal.
public static bool operator ==(DiscordChannel e1, DiscordChannel e2)
{
var o1 = e1 as object;
var o2 = e2 as object;
return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || e1.Id == e2.Id);
}
///
/// Gets whether the two objects are not equal.
///
/// First channel to compare.
/// Second channel to compare.
/// Whether the two channels are not equal.
public static bool operator !=(DiscordChannel e1, DiscordChannel e2)
=> !(e1 == e2);
}
diff --git a/DisCatSharp/Entities/Interaction/Components/Select/DiscordSelectComponent.cs b/DisCatSharp/Entities/Interaction/Components/Select/DiscordBaseSelectComponent.cs
similarity index 70%
copy from DisCatSharp/Entities/Interaction/Components/Select/DiscordSelectComponent.cs
copy to DisCatSharp/Entities/Interaction/Components/Select/DiscordBaseSelectComponent.cs
index 754faeee7..59960d999 100644
--- a/DisCatSharp/Entities/Interaction/Components/Select/DiscordSelectComponent.cs
+++ b/DisCatSharp/Entities/Interaction/Components/Select/DiscordBaseSelectComponent.cs
@@ -1,143 +1,111 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
-using System.Collections.Generic;
-using System.Linq;
using DisCatSharp.Enums;
using Newtonsoft.Json;
namespace DisCatSharp.Entities;
///
/// A select menu with multiple options to choose from.
///
-public sealed class DiscordSelectComponent : DiscordComponent
+public class DiscordBaseSelectComponent : DiscordComponent
{
- ///
- /// The options to pick from on this component.
- ///
- [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)]
- public IReadOnlyList Options { get; internal set; } = Array.Empty();
-
///
/// The text to show when no option is selected.
///
[JsonProperty("placeholder", NullValueHandling = NullValueHandling.Ignore)]
public string Placeholder { get; internal set; }
///
/// The minimum amount of options that can be selected. Must be less than or equal to . Defaults to one.
///
[JsonProperty("min_values", NullValueHandling = NullValueHandling.Ignore)]
public int? MinimumSelectedValues { get; internal set; } = 1;
///
/// The maximum amount of options that can be selected. Must be greater than or equal to zero or . Defaults to one.
///
[JsonProperty("max_values", NullValueHandling = NullValueHandling.Ignore)]
public int? MaximumSelectedValues { get; internal set; } = 1;
///
/// Whether this select can be used.
///
[JsonProperty("disabled", NullValueHandling = NullValueHandling.Ignore)]
public bool Disabled { get; internal set; }
///
/// Label of component, if used in modal.
///
[JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)]
public string Label { get; internal set; } = null;
- ///
- /// Enables this component if it was disabled before.
- ///
- /// The current component.
- public DiscordSelectComponent Enable()
- {
- this.Disabled = false;
- return this;
- }
-
- ///
- /// Disables this component.
- ///
- /// The current component.
- public DiscordSelectComponent Disable()
- {
- this.Disabled = true;
- return this;
- }
-
- // TODO: Can we set required
///
- /// Constructs a new .
+ /// Constructs a new .
///
+ /// The type of select.
/// Text to show if no option is selected.
- /// Array of options
/// The Id to assign to the select component.
/// Minimum count of selectable options.
/// Maximum count of selectable options.
/// Whether this select component should be initialized as being disabled. User sees a greyed out select component that cannot be interacted with.
- public DiscordSelectComponent(string placeholder, IEnumerable options, string customId = null, int minOptions = 1, int maxOptions = 1, bool disabled = false) : this()
+ internal DiscordBaseSelectComponent(ComponentType type, string placeholder, string customId = null, int minOptions = 1, int maxOptions = 1, bool disabled = false)
{
+ this.Type = type;
this.CustomId = customId ?? Guid.NewGuid().ToString(); ;
this.Disabled = disabled;
- this.Options = options.ToArray();
this.Placeholder = placeholder;
this.MinimumSelectedValues = minOptions;
this.MaximumSelectedValues = maxOptions;
}
///
- /// Constructs a new for modals.
+ /// Constructs a new for modals.
///
+ /// The type of select.
/// Maximum count of selectable options.
/// Text to show if no option is selected.
- /// Array of options
/// The Id to assign to the select component.
/// Minimum count of selectable options.
/// Maximum count of selectable options.
/// Whether this select component should be initialized as being disabled. User sees a greyed out select component that cannot be interacted with.
- public DiscordSelectComponent(string label, string placeholder, IEnumerable options, string customId = null, int minOptions = 1, int maxOptions = 1, bool disabled = false) : this()
+ internal DiscordBaseSelectComponent(ComponentType type, string label, string placeholder, string customId = null, int minOptions = 1, int maxOptions = 1, bool disabled = false)
{
+ this.Type = type;
this.Label = label;
this.CustomId = customId ?? Guid.NewGuid().ToString(); ;
this.Disabled = disabled;
- this.Options = options.ToArray();
this.Placeholder = placeholder;
this.MinimumSelectedValues = minOptions;
this.MaximumSelectedValues = maxOptions;
}
- ///
- /// Initializes a new instance of the class.
- ///
- public DiscordSelectComponent()
+ internal DiscordBaseSelectComponent()
{
- this.Type = ComponentType.Select;
+
}
}
diff --git a/DisCatSharp/Entities/Interaction/Components/Select/DiscordSelectComponent.cs b/DisCatSharp/Entities/Interaction/Components/Select/DiscordChannelSelectComponent.cs
similarity index 50%
copy from DisCatSharp/Entities/Interaction/Components/Select/DiscordSelectComponent.cs
copy to DisCatSharp/Entities/Interaction/Components/Select/DiscordChannelSelectComponent.cs
index 754faeee7..ccd48e3e8 100644
--- a/DisCatSharp/Entities/Interaction/Components/Select/DiscordSelectComponent.cs
+++ b/DisCatSharp/Entities/Interaction/Components/Select/DiscordChannelSelectComponent.cs
@@ -1,143 +1,103 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
-using System;
using System.Collections.Generic;
using System.Linq;
using DisCatSharp.Enums;
using Newtonsoft.Json;
namespace DisCatSharp.Entities;
///
/// A select menu with multiple options to choose from.
///
-public sealed class DiscordSelectComponent : DiscordComponent
+public sealed class DiscordChannelSelectComponent : DiscordBaseSelectComponent
{
///
- /// The options to pick from on this component.
+ /// The channel types to filter by.
///
- [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)]
- public IReadOnlyList Options { get; internal set; } = Array.Empty();
-
- ///
- /// The text to show when no option is selected.
- ///
- [JsonProperty("placeholder", NullValueHandling = NullValueHandling.Ignore)]
- public string Placeholder { get; internal set; }
-
- ///
- /// The minimum amount of options that can be selected. Must be less than or equal to . Defaults to one.
- ///
- [JsonProperty("min_values", NullValueHandling = NullValueHandling.Ignore)]
- public int? MinimumSelectedValues { get; internal set; } = 1;
-
- ///
- /// The maximum amount of options that can be selected. Must be greater than or equal to zero or . Defaults to one.
- ///
- [JsonProperty("max_values", NullValueHandling = NullValueHandling.Ignore)]
- public int? MaximumSelectedValues { get; internal set; } = 1;
-
- ///
- /// Whether this select can be used.
- ///
- [JsonProperty("disabled", NullValueHandling = NullValueHandling.Ignore)]
- public bool Disabled { get; internal set; }
-
- ///
- /// Label of component, if used in modal.
- ///
- [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)]
- public string Label { get; internal set; } = null;
+ [JsonProperty("channel_types", NullValueHandling = NullValueHandling.Ignore)]
+ public IReadOnlyList ChannelTypes { get; internal set; } = null;
///
/// Enables this component if it was disabled before.
///
/// The current component.
- public DiscordSelectComponent Enable()
+ public DiscordChannelSelectComponent Enable()
{
this.Disabled = false;
return this;
}
///
/// Disables this component.
///
/// The current component.
- public DiscordSelectComponent Disable()
+ public DiscordChannelSelectComponent Disable()
{
this.Disabled = true;
return this;
}
// TODO: Can we set required
///
- /// Constructs a new .
+ /// Constructs a new .
///
/// Text to show if no option is selected.
- /// Array of options
+ /// The channel types to filter by.
/// The Id to assign to the select component.
/// Minimum count of selectable options.
/// Maximum count of selectable options.
/// Whether this select component should be initialized as being disabled. User sees a greyed out select component that cannot be interacted with.
- public DiscordSelectComponent(string placeholder, IEnumerable options, string customId = null, int minOptions = 1, int maxOptions = 1, bool disabled = false) : this()
+ public DiscordChannelSelectComponent(string placeholder, IEnumerable channelTypes = null, string customId = null, int minOptions = 1, int maxOptions = 1, bool disabled = false)
+ : base(ComponentType.ChannelSelect, placeholder, customId, minOptions, maxOptions, disabled)
{
- this.CustomId = customId ?? Guid.NewGuid().ToString(); ;
- this.Disabled = disabled;
- this.Options = options.ToArray();
- this.Placeholder = placeholder;
- this.MinimumSelectedValues = minOptions;
- this.MaximumSelectedValues = maxOptions;
+ this.ChannelTypes = channelTypes.ToArray();
}
///
- /// Constructs a new for modals.
+ /// Constructs a new for modals.
///
/// Maximum count of selectable options.
/// Text to show if no option is selected.
- /// Array of options
+ /// The channel types to filter by.
/// The Id to assign to the select component.
/// Minimum count of selectable options.
/// Maximum count of selectable options.
/// Whether this select component should be initialized as being disabled. User sees a greyed out select component that cannot be interacted with.
- public DiscordSelectComponent(string label, string placeholder, IEnumerable options, string customId = null, int minOptions = 1, int maxOptions = 1, bool disabled = false) : this()
+ public DiscordChannelSelectComponent(string label, string placeholder, IEnumerable channelTypes = null, string customId = null, int minOptions = 1, int maxOptions = 1, bool disabled = false)
+ : base(ComponentType.RoleSelect, label, placeholder, customId, minOptions, maxOptions, disabled)
{
- this.Label = label;
- this.CustomId = customId ?? Guid.NewGuid().ToString(); ;
- this.Disabled = disabled;
- this.Options = options.ToArray();
- this.Placeholder = placeholder;
- this.MinimumSelectedValues = minOptions;
- this.MaximumSelectedValues = maxOptions;
+ this.ChannelTypes = channelTypes.ToArray();
}
///
- /// Initializes a new instance of the class.
+ /// Constructs a new .
///
- public DiscordSelectComponent()
+ public DiscordChannelSelectComponent() : base()
{
- this.Type = ComponentType.Select;
+ this.Type = ComponentType.ChannelSelect;
}
}
diff --git a/DisCatSharp/Entities/Interaction/Components/Select/DiscordMentionableSelectComponent.cs b/DisCatSharp/Entities/Interaction/Components/Select/DiscordMentionableSelectComponent.cs
new file mode 100644
index 000000000..e8ea5cb44
--- /dev/null
+++ b/DisCatSharp/Entities/Interaction/Components/Select/DiscordMentionableSelectComponent.cs
@@ -0,0 +1,86 @@
+// This file is part of the DisCatSharp project, based off DSharpPlus.
+//
+// Copyright (c) 2021-2022 AITSYS
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+using DisCatSharp.Enums;
+
+namespace DisCatSharp.Entities;
+
+///
+/// A select menu with multiple options to choose from.
+///
+public sealed class DiscordMentionableSelectComponent : DiscordBaseSelectComponent
+{
+ ///
+ /// Enables this component if it was disabled before.
+ ///
+ /// The current component.
+ public DiscordMentionableSelectComponent Enable()
+ {
+ this.Disabled = false;
+ return this;
+ }
+
+ ///
+ /// Disables this component.
+ ///
+ /// The current component.
+ public DiscordMentionableSelectComponent Disable()
+ {
+ this.Disabled = true;
+ return this;
+ }
+
+ // TODO: Can we set required
+
+ ///
+ /// Constructs a new .
+ ///
+ /// Text to show if no option is selected.
+ /// The Id to assign to the select component.
+ /// Minimum count of selectable options.
+ /// Maximum count of selectable options.
+ /// Whether this select component should be initialized as being disabled. User sees a greyed out select component that cannot be interacted with.
+ public DiscordMentionableSelectComponent(string placeholder, string customId = null, int minOptions = 1, int maxOptions = 1, bool disabled = false)
+ : base(ComponentType.MentionableSelect, placeholder, customId, minOptions, maxOptions, disabled)
+ { }
+
+ ///
+ /// Constructs a new for modals.
+ ///
+ /// Maximum count of selectable options.
+ /// Text to show if no option is selected.
+ /// The Id to assign to the select component.
+ /// Minimum count of selectable options.
+ /// Maximum count of selectable options.
+ /// Whether this select component should be initialized as being disabled. User sees a greyed out select component that cannot be interacted with.
+ public DiscordMentionableSelectComponent(string label, string placeholder, string customId = null, int minOptions = 1, int maxOptions = 1, bool disabled = false)
+ : base(ComponentType.MentionableSelect, label, placeholder, customId, minOptions, maxOptions, disabled)
+ { }
+
+ ///
+ /// Constructs a new .
+ ///
+ public DiscordMentionableSelectComponent() : base()
+ {
+ this.Type = ComponentType.MentionableSelect;
+ }
+}
diff --git a/DisCatSharp/Entities/Interaction/Components/Select/DiscordRoleSelectComponent.cs b/DisCatSharp/Entities/Interaction/Components/Select/DiscordRoleSelectComponent.cs
new file mode 100644
index 000000000..dd351f918
--- /dev/null
+++ b/DisCatSharp/Entities/Interaction/Components/Select/DiscordRoleSelectComponent.cs
@@ -0,0 +1,86 @@
+// This file is part of the DisCatSharp project, based off DSharpPlus.
+//
+// Copyright (c) 2021-2022 AITSYS
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+using DisCatSharp.Enums;
+
+namespace DisCatSharp.Entities;
+
+///
+/// A select menu with multiple options to choose from.
+///
+public sealed class DiscordRoleSelectComponent : DiscordBaseSelectComponent
+{
+ ///
+ /// Enables this component if it was disabled before.
+ ///
+ /// The current component.
+ public DiscordRoleSelectComponent Enable()
+ {
+ this.Disabled = false;
+ return this;
+ }
+
+ ///
+ /// Disables this component.
+ ///
+ /// The current component.
+ public DiscordRoleSelectComponent Disable()
+ {
+ this.Disabled = true;
+ return this;
+ }
+
+ // TODO: Can we set required
+
+ ///
+ /// Constructs a new .
+ ///
+ /// Text to show if no option is selected.
+ /// The Id to assign to the select component.
+ /// Minimum count of selectable options.
+ /// Maximum count of selectable options.
+ /// Whether this select component should be initialized as being disabled. User sees a greyed out select component that cannot be interacted with.
+ public DiscordRoleSelectComponent(string placeholder, string customId = null, int minOptions = 1, int maxOptions = 1, bool disabled = false)
+ : base(ComponentType.RoleSelect, placeholder, customId, minOptions, maxOptions, disabled)
+ { }
+
+ ///
+ /// Constructs a new for modals.
+ ///
+ /// Maximum count of selectable options.
+ /// Text to show if no option is selected.
+ /// The Id to assign to the select component.
+ /// Minimum count of selectable options.
+ /// Maximum count of selectable options.
+ /// Whether this select component should be initialized as being disabled. User sees a greyed out select component that cannot be interacted with.
+ public DiscordRoleSelectComponent(string label, string placeholder, string customId = null, int minOptions = 1, int maxOptions = 1, bool disabled = false)
+ : base(ComponentType.RoleSelect, label, placeholder, customId, minOptions, maxOptions, disabled)
+ { }
+
+ ///
+ /// Constructs a new .
+ ///
+ public DiscordRoleSelectComponent() : base()
+ {
+ this.Type = ComponentType.RoleSelect;
+ }
+}
diff --git a/DisCatSharp/Entities/Interaction/Components/Select/DiscordSelectComponent.cs b/DisCatSharp/Entities/Interaction/Components/Select/DiscordStringSelectComponent.cs
similarity index 54%
rename from DisCatSharp/Entities/Interaction/Components/Select/DiscordSelectComponent.cs
rename to DisCatSharp/Entities/Interaction/Components/Select/DiscordStringSelectComponent.cs
index 754faeee7..0cfd579d1 100644
--- a/DisCatSharp/Entities/Interaction/Components/Select/DiscordSelectComponent.cs
+++ b/DisCatSharp/Entities/Interaction/Components/Select/DiscordStringSelectComponent.cs
@@ -1,143 +1,102 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Linq;
using DisCatSharp.Enums;
using Newtonsoft.Json;
namespace DisCatSharp.Entities;
///
/// A select menu with multiple options to choose from.
///
-public sealed class DiscordSelectComponent : DiscordComponent
+public sealed class DiscordStringSelectComponent : DiscordBaseSelectComponent
{
///
/// The options to pick from on this component.
///
[JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)]
- public IReadOnlyList Options { get; internal set; } = Array.Empty();
-
- ///
- /// The text to show when no option is selected.
- ///
- [JsonProperty("placeholder", NullValueHandling = NullValueHandling.Ignore)]
- public string Placeholder { get; internal set; }
-
- ///
- /// The minimum amount of options that can be selected. Must be less than or equal to . Defaults to one.
- ///
- [JsonProperty("min_values", NullValueHandling = NullValueHandling.Ignore)]
- public int? MinimumSelectedValues { get; internal set; } = 1;
-
- ///
- /// The maximum amount of options that can be selected. Must be greater than or equal to zero or . Defaults to one.
- ///
- [JsonProperty("max_values", NullValueHandling = NullValueHandling.Ignore)]
- public int? MaximumSelectedValues { get; internal set; } = 1;
-
- ///
- /// Whether this select can be used.
- ///
- [JsonProperty("disabled", NullValueHandling = NullValueHandling.Ignore)]
- public bool Disabled { get; internal set; }
-
- ///
- /// Label of component, if used in modal.
- ///
- [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)]
- public string Label { get; internal set; } = null;
+ public IReadOnlyList Options { get; internal set; } = Array.Empty();
///
/// Enables this component if it was disabled before.
///
/// The current component.
- public DiscordSelectComponent Enable()
+ public DiscordStringSelectComponent Enable()
{
this.Disabled = false;
return this;
}
///
/// Disables this component.
///
/// The current component.
- public DiscordSelectComponent Disable()
+ public DiscordStringSelectComponent Disable()
{
this.Disabled = true;
return this;
}
- // TODO: Can we set required
-
///
- /// Constructs a new .
+ /// Constructs a new .
///
/// Text to show if no option is selected.
/// Array of options
/// The Id to assign to the select component.
/// Minimum count of selectable options.
/// Maximum count of selectable options.
/// Whether this select component should be initialized as being disabled. User sees a greyed out select component that cannot be interacted with.
- public DiscordSelectComponent(string placeholder, IEnumerable options, string customId = null, int minOptions = 1, int maxOptions = 1, bool disabled = false) : this()
+ public DiscordStringSelectComponent(string placeholder, IEnumerable options, string customId = null, int minOptions = 1, int maxOptions = 1, bool disabled = false)
+ : base(ComponentType.StringSelect, placeholder, customId, minOptions, maxOptions, disabled)
{
- this.CustomId = customId ?? Guid.NewGuid().ToString(); ;
- this.Disabled = disabled;
this.Options = options.ToArray();
- this.Placeholder = placeholder;
- this.MinimumSelectedValues = minOptions;
- this.MaximumSelectedValues = maxOptions;
}
///
- /// Constructs a new for modals.
+ /// Constructs a new for modals.
///
/// Maximum count of selectable options.
/// Text to show if no option is selected.
/// Array of options
/// The Id to assign to the select component.
/// Minimum count of selectable options.
/// Maximum count of selectable options.
/// Whether this select component should be initialized as being disabled. User sees a greyed out select component that cannot be interacted with.
- public DiscordSelectComponent(string label, string placeholder, IEnumerable options, string customId = null, int minOptions = 1, int maxOptions = 1, bool disabled = false) : this()
+ public DiscordStringSelectComponent(string label, string placeholder, IEnumerable options, string customId = null, int minOptions = 1, int maxOptions = 1, bool disabled = false)
+ : base(ComponentType.StringSelect, label, placeholder, customId, minOptions, maxOptions, disabled)
{
- this.Label = label;
- this.CustomId = customId ?? Guid.NewGuid().ToString(); ;
- this.Disabled = disabled;
this.Options = options.ToArray();
- this.Placeholder = placeholder;
- this.MinimumSelectedValues = minOptions;
- this.MaximumSelectedValues = maxOptions;
}
///
- /// Initializes a new instance of the class.
+ /// Constructs a new .
///
- public DiscordSelectComponent()
+ public DiscordStringSelectComponent() : base()
{
- this.Type = ComponentType.Select;
+ this.Type = ComponentType.StringSelect;
}
}
diff --git a/DisCatSharp/Entities/Interaction/Components/Select/DiscordSelectComponentOption.cs b/DisCatSharp/Entities/Interaction/Components/Select/DiscordStringSelectComponentOption.cs
similarity index 90%
rename from DisCatSharp/Entities/Interaction/Components/Select/DiscordSelectComponentOption.cs
rename to DisCatSharp/Entities/Interaction/Components/Select/DiscordStringSelectComponentOption.cs
index 14f2a2c8d..cfe4bd36b 100644
--- a/DisCatSharp/Entities/Interaction/Components/Select/DiscordSelectComponentOption.cs
+++ b/DisCatSharp/Entities/Interaction/Components/Select/DiscordStringSelectComponentOption.cs
@@ -1,87 +1,87 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using Newtonsoft.Json;
namespace DisCatSharp.Entities;
///
-/// Represents options for .
+/// Represents options for .
///
-public sealed class DiscordSelectComponentOption
+public sealed class DiscordStringSelectComponentOption
{
///
/// The label to add. This is required.
///
[JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)]
public string Label { get; internal set; }
///
/// The value of this option. Akin to the Custom Id of components.
///
[JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)]
public string Value { get; internal set; }
///
/// Whether this option is default. If true, this option will be pre-selected. Defaults to false.
///
[JsonProperty("default", NullValueHandling = NullValueHandling.Ignore)]
public bool Default { get; internal set; } // false //
///
/// The description of this option. This is optional.
///
[JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)]
public string Description { get; internal set; }
///
/// The emoji of this option. This is optional.
///
[JsonProperty("emoji", NullValueHandling = NullValueHandling.Ignore)]
public DiscordComponentEmoji Emoji { get; internal set; }
///
- /// Constructs a new .
+ /// Constructs a new .
///
/// The label of this option.
/// The value of this option.
/// Description of the option.
/// Whether this option is default. If true, this option will be pre-selected.
/// The emoji to set with this option.
- public DiscordSelectComponentOption(string label, string value, string description = null, bool isDefault = false, DiscordComponentEmoji emoji = null)
+ public DiscordStringSelectComponentOption(string label, string value, string description = null, bool isDefault = false, DiscordComponentEmoji emoji = null)
{
if (label.Length > 100)
throw new NotSupportedException("Select label can't be longer then 100 chars.");
if (value.Length > 100)
throw new NotSupportedException("Select value can't be longer then 100 chars.");
if (description != null && description.Length > 100)
throw new NotSupportedException("Select description can't be longer then 100 chars.");
this.Label = label;
this.Value = value;
this.Description = description;
this.Default = isDefault;
this.Emoji = emoji;
}
}
diff --git a/DisCatSharp/Entities/Interaction/Components/Select/DiscordUserSelectComponent.cs b/DisCatSharp/Entities/Interaction/Components/Select/DiscordUserSelectComponent.cs
new file mode 100644
index 000000000..5ed33c4d6
--- /dev/null
+++ b/DisCatSharp/Entities/Interaction/Components/Select/DiscordUserSelectComponent.cs
@@ -0,0 +1,84 @@
+// This file is part of the DisCatSharp project, based off DSharpPlus.
+//
+// Copyright (c) 2021-2022 AITSYS
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+using DisCatSharp.Enums;
+
+namespace DisCatSharp.Entities;
+
+///
+/// A select menu with multiple options to choose from.
+///
+public sealed class DiscordUserSelectComponent : DiscordBaseSelectComponent
+{
+ ///
+ /// Enables this component if it was disabled before.
+ ///
+ /// The current component.
+ public DiscordUserSelectComponent Enable()
+ {
+ this.Disabled = false;
+ return this;
+ }
+
+ ///
+ /// Disables this component.
+ ///
+ /// The current component.
+ public DiscordUserSelectComponent Disable()
+ {
+ this.Disabled = true;
+ return this;
+ }
+
+ ///
+ /// Constructs a new .
+ ///
+ /// Text to show if no option is selected.
+ /// The Id to assign to the select component.
+ /// Minimum count of selectable options.
+ /// Maximum count of selectable options.
+ /// Whether this select component should be initialized as being disabled. User sees a greyed out select component that cannot be interacted with.
+ public DiscordUserSelectComponent(string placeholder, string customId = null, int minOptions = 1, int maxOptions = 1, bool disabled = false)
+ : base(ComponentType.UserSelect, placeholder, customId, minOptions, maxOptions, disabled)
+ { }
+
+ ///
+ /// Constructs a new for modals.
+ ///
+ /// Maximum count of selectable options.
+ /// Text to show if no option is selected.
+ /// The Id to assign to the select component.
+ /// Minimum count of selectable options.
+ /// Maximum count of selectable options.
+ /// Whether this select component should be initialized as being disabled. User sees a greyed out select component that cannot be interacted with.
+ public DiscordUserSelectComponent(string label, string placeholder, string customId = null, int minOptions = 1, int maxOptions = 1, bool disabled = false)
+ : base(ComponentType.UserSelect, label, placeholder, customId, minOptions, maxOptions, disabled)
+ { }
+
+ ///
+ /// Constructs a new .
+ ///
+ public DiscordUserSelectComponent() : base()
+ {
+ this.Type = ComponentType.UserSelect;
+ }
+}
diff --git a/DisCatSharp/Entities/Interaction/DiscordInteractionModalBuilder.cs b/DisCatSharp/Entities/Interaction/DiscordInteractionModalBuilder.cs
index 4e2263530..4e8f8c36c 100644
--- a/DisCatSharp/Entities/Interaction/DiscordInteractionModalBuilder.cs
+++ b/DisCatSharp/Entities/Interaction/DiscordInteractionModalBuilder.cs
@@ -1,183 +1,183 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Linq;
namespace DisCatSharp.Entities;
///
/// Constructs an interaction modal response.
///
public sealed class DiscordInteractionModalBuilder
{
///
/// Title of modal.
///
public string Title
{
get => this._title;
set
{
if (value != null && value.Length > 128)
throw new ArgumentException("Title length cannot exceed 128 characters.", nameof(value));
this._title = value;
}
}
private string _title;
///
/// Custom id of modal.
///
public string CustomId { get; set; }
///
/// Components to send on this interaction response.
///
public IReadOnlyList ModalComponents => this._components;
private readonly List _components = new();
///
/// Constructs a new empty interaction modal builder.
///
public DiscordInteractionModalBuilder(string title = null, string customId = null)
{
this.Title = title ?? "Title";
this.CustomId = customId ?? Guid.NewGuid().ToString();
}
public DiscordInteractionModalBuilder WithTitle(string title)
{
this.Title = title;
return this;
}
public DiscordInteractionModalBuilder WithCustomId(string customId)
{
this.CustomId = customId;
return this;
}
///
/// Appends a collection of text components to the builder. Each call will append to a new row.
///
/// The components to append. Up to five.
/// The current builder to chain calls with.
/// Thrown when passing more than 5 components.
public DiscordInteractionModalBuilder AddTextComponents(params DiscordTextComponent[] components)
=> this.AddModalComponents(components);
///
/// Appends a collection of select components to the builder. Each call will append to a new row.
///
/// The components to append. Up to five.
/// The current builder to chain calls with.
/// Thrown when passing more than 5 components.
- public DiscordInteractionModalBuilder AddSelectComponents(params DiscordSelectComponent[] components)
+ public DiscordInteractionModalBuilder AddSelectComponents(params DiscordBaseSelectComponent[] components)
=> this.AddModalComponents(components);
///
/// Appends a text component to the builder.
///
/// The component to append.
/// The current builder to chain calls with.
public DiscordInteractionModalBuilder AddTextComponent(DiscordTextComponent component)
=> this.AddModalComponents(component);
///
/// Appends a select component to the builder.
///
/// The component to append.
/// The current builder to chain calls with.
- public DiscordInteractionModalBuilder AddSelectComponent(DiscordSelectComponent component)
+ public DiscordInteractionModalBuilder AddSelectComponent(DiscordBaseSelectComponent component)
=> this.AddModalComponents(component);
///
/// Appends a collection of components to the builder.
///
/// The components to append. Up to five.
/// The current builder to chain calls with.
/// Thrown when passing more than 5 components.
public DiscordInteractionModalBuilder AddModalComponents(params DiscordComponent[] components)
{
var ara = components.ToArray();
if (ara.Length > 5)
throw new ArgumentException("You can only add 5 components to modals.");
if (this._components.Count + ara.Length > 5)
throw new ArgumentException($"You try to add too many components. We already have {this._components.Count}.");
foreach (var ar in ara)
this._components.Add(new DiscordActionRowComponent(new List() { ar }));
return this;
}
///
/// Appends several rows of components to the message
///
/// The rows of components to add, holding up to five each.
/// The current builder to chain calls with.
/// Thrown when passing more than 5 components.
public DiscordInteractionModalBuilder AddModalComponents(IEnumerable components)
{
var ara = components.ToArray();
if (ara.Length + this._components.Count > 5)
throw new ArgumentException("ActionRow count exceeds maximum of five.");
foreach (var ar in ara)
this._components.Add(ar);
return this;
}
///
/// Appends a collection of components to the builder. Each call will append to a new row.
///
/// The component to append.
/// The current builder to chain calls with.
internal DiscordInteractionModalBuilder AddModalComponents(DiscordComponent component)
{
this._components.Add(new DiscordActionRowComponent(new List() { component }));
return this;
}
///
/// Clears all message components on this builder.
///
public void ClearComponents()
=> this._components.Clear();
///
/// Allows for clearing the Interaction Response Builder so that it can be used again to send a new response.
///
public void Clear()
{
this._components.Clear();
this.Title = null;
this.CustomId = null;
}
}
diff --git a/DisCatSharp/Enums/Interaction/ComponentType.cs b/DisCatSharp/Enums/Interaction/ComponentType.cs
index 2a81ddb2c..8c5cac979 100644
--- a/DisCatSharp/Enums/Interaction/ComponentType.cs
+++ b/DisCatSharp/Enums/Interaction/ComponentType.cs
@@ -1,49 +1,69 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
namespace DisCatSharp.Enums;
///
/// Represents a type of component.
///
public enum ComponentType
{
///
/// A row of components.
///
ActionRow = 1,
///
/// A button.
///
Button = 2,
///
- /// A select menu.
+ /// A select menu to select strings.
///
- Select = 3,
+ StringSelect = 3,
///
/// A input text.
///
- InputText = 4
+ InputText = 4,
+
+ ///
+ /// A select menu to select users.
+ ///
+ UserSelect = 5,
+
+ ///
+ /// A select menu to select roles.
+ ///
+ RoleSelect = 6,
+
+ ///
+ /// A select menu to select menu to select users and roles.
+ ///
+ MentionableSelect = 7,
+
+ ///
+ /// A select menu to select channels.
+ ///
+ ChannelSelect = 8,
}
diff --git a/DisCatSharp/Net/Rest/DiscordApiClient.cs b/DisCatSharp/Net/Rest/DiscordApiClient.cs
index 587d39b70..e654203bf 100644
--- a/DisCatSharp/Net/Rest/DiscordApiClient.cs
+++ b/DisCatSharp/Net/Rest/DiscordApiClient.cs
@@ -1,5494 +1,5494 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.Net.Abstractions;
using DisCatSharp.Net.Serialization;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace DisCatSharp.Net;
///
/// Represents a discord api client.
///
public sealed class DiscordApiClient
{
///
/// The audit log reason header name.
///
private const string REASON_HEADER_NAME = "X-Audit-Log-Reason";
///
/// Gets the discord client.
///
internal BaseDiscordClient Discord { get; }
///
/// Gets the rest client.
///
internal RestClient Rest { get; }
///
/// Initializes a new instance of the class.
///
/// The client.
internal DiscordApiClient(BaseDiscordClient client)
{
this.Discord = client;
this.Rest = new RestClient(client);
}
///
/// Initializes a new instance of the class.
///
/// The proxy.
/// The timeout.
/// If true, use relative rate limit.
/// The logger.
internal DiscordApiClient(IWebProxy proxy, TimeSpan timeout, bool useRelativeRateLimit, ILogger logger) // This is for meta-clients, such as the webhook client
{
this.Rest = new RestClient(proxy, timeout, useRelativeRateLimit, logger);
}
///
/// Builds the query string.
///
/// The values.
/// Whether this query will be transmitted via POST.
private static string BuildQueryString(IDictionary values, bool post = false)
{
if (values == null || values.Count == 0)
return string.Empty;
var valsCollection = values.Select(xkvp =>
$"{WebUtility.UrlEncode(xkvp.Key)}={WebUtility.UrlEncode(xkvp.Value)}");
var vals = string.Join("&", valsCollection);
return !post ? $"?{vals}" : vals;
}
///
/// Prepares the message.
///
/// The msg_raw.
/// A DiscordMessage.
private DiscordMessage PrepareMessage(JToken msgRaw)
{
var author = msgRaw["author"].ToObject();
var ret = msgRaw.ToDiscordObject();
ret.Discord = this.Discord;
this.PopulateMessage(author, ret);
var referencedMsg = msgRaw["referenced_message"];
if (ret.MessageType == MessageType.Reply && !string.IsNullOrWhiteSpace(referencedMsg?.ToString()))
{
author = referencedMsg["author"].ToObject();
ret.ReferencedMessage.Discord = this.Discord;
this.PopulateMessage(author, ret.ReferencedMessage);
}
if (ret.Channel != null)
return ret;
var channel = !ret.GuildId.HasValue
? new DiscordDmChannel
{
Id = ret.ChannelId,
Discord = this.Discord,
Type = ChannelType.Private
}
: new DiscordChannel
{
Id = ret.ChannelId,
GuildId = ret.GuildId,
Discord = this.Discord
};
ret.Channel = channel;
return ret;
}
///
/// Populates the message.
///
/// The author.
/// The message.
private void PopulateMessage(TransportUser author, DiscordMessage ret)
{
var guild = ret.Channel?.Guild;
//If this is a webhook, it shouldn't be in the user cache.
if (author.IsBot && int.Parse(author.Discriminator) == 0)
{
ret.Author = new DiscordUser(author) { Discord = this.Discord };
}
else
{
if (!this.Discord.UserCache.TryGetValue(author.Id, out var usr))
{
this.Discord.UserCache[author.Id] = usr = new DiscordUser(author) { Discord = this.Discord };
}
if (guild != null)
{
if (!guild.Members.TryGetValue(author.Id, out var mbr))
mbr = new DiscordMember(usr) { Discord = this.Discord, GuildId = guild.Id };
ret.Author = mbr;
}
else
{
ret.Author = usr;
}
}
ret.PopulateMentions();
ret.ReactionsInternal ??= new List();
foreach (var xr in ret.ReactionsInternal)
xr.Emoji.Discord = this.Discord;
}
///
/// Executes a rest request.
///
/// The client.
/// The bucket.
/// The url.
/// The method.
/// The route.
/// The headers.
/// The payload.
/// The ratelimit wait override.
internal Task DoRequestAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, string payload = null, double? ratelimitWaitOverride = null)
{
var req = new RestRequest(client, bucket, url, method, route, headers, payload, ratelimitWaitOverride);
if (this.Discord != null)
this.Rest.ExecuteRequestAsync(req).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request");
else
_ = this.Rest.ExecuteRequestAsync(req);
return req.WaitForCompletionAsync();
}
///
/// Executes a multipart rest request for stickers.
///
/// The client.
/// The bucket.
/// The url.
/// The method.
/// The route.
/// The headers.
/// The file.
/// The sticker name.
/// The sticker tag.
/// The sticker description.
/// The ratelimit wait override.
private Task DoStickerMultipartAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null,
DiscordMessageFile file = null, string name = "", string tags = "", string description = "", double? ratelimitWaitOverride = null)
{
var req = new MultipartStickerWebRequest(client, bucket, url, method, route, headers, file, name, tags, description, ratelimitWaitOverride);
if (this.Discord != null)
this.Rest.ExecuteRequestAsync(req).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request");
else
_ = this.Rest.ExecuteRequestAsync(req);
return req.WaitForCompletionAsync();
}
///
/// Executes a multipart request.
///
/// The client.
/// The bucket.
/// The url.
/// The method.
/// The route.
/// The headers.
/// The values.
/// The files.
/// The ratelimit wait override.
private Task DoMultipartAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, IReadOnlyDictionary values = null,
IReadOnlyCollection files = null, double? ratelimitWaitOverride = null)
{
var req = new MultipartWebRequest(client, bucket, url, method, route, headers, values, files, ratelimitWaitOverride);
if (this.Discord != null)
this.Rest.ExecuteRequestAsync(req).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request");
else
_ = this.Rest.ExecuteRequestAsync(req);
return req.WaitForCompletionAsync();
}
#region Guild
///
/// Searches the members async.
///
/// The guild_id.
/// The name.
/// The limit.
internal async Task> SearchMembersAsync(ulong guildId, string name, int? limit)
{
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}{Endpoints.SEARCH}";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path);
var querydict = new Dictionary
{
["query"] = name,
["limit"] = limit.ToString()
};
var url = Utilities.GetApiUriFor(path, BuildQueryString(querydict), this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false);
var json = JArray.Parse(res.Response);
var tms = json.ToObject>();
var mbrs = new List();
foreach (var xtm in tms)
{
var usr = new DiscordUser(xtm.User) { Discord = this.Discord };
this.Discord.UserCache.AddOrUpdate(xtm.User.Id, usr, (id, old) =>
{
old.Username = usr.Username;
old.Discord = usr.Discord;
old.AvatarHash = usr.AvatarHash;
return old;
});
mbrs.Add(new DiscordMember(xtm) { Discord = this.Discord, GuildId = guildId });
}
return mbrs;
}
///
/// Gets the guild ban async.
///
/// The guild_id.
/// The user_id.
internal async Task GetGuildBanAsync(ulong guildId, ulong userId)
{
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId, user_id = userId}, out var path);
var uri = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, uri, RestRequestMethod.GET, route).ConfigureAwait(false);
var json = JObject.Parse(res.Response);
var ban = json.ToObject();
return ban;
}
///
/// Creates the guild async.
///
/// The name.
/// The region_id.
/// The iconb64.
/// The verification_level.
/// The default_message_notifications.
/// The system_channel_flags.
internal async Task CreateGuildAsync(string name, string regionId, Optional iconb64, VerificationLevel? verificationLevel,
DefaultMessageNotifications? defaultMessageNotifications, SystemChannelFlags? systemChannelFlags)
{
var pld = new RestGuildCreatePayload
{
Name = name,
RegionId = regionId,
DefaultMessageNotifications = defaultMessageNotifications,
VerificationLevel = verificationLevel,
IconBase64 = iconb64,
SystemChannelFlags = systemChannelFlags
};
var route = $"{Endpoints.GUILDS}";
var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false);
var json = JObject.Parse(res.Response);
var rawMembers = (JArray)json["members"];
var guild = json.ToDiscordObject();
if (this.Discord is DiscordClient dc)
await dc.OnGuildCreateEventAsync(guild, rawMembers, null).ConfigureAwait(false);
return guild;
}
///
/// Creates the guild from template async.
///
/// The template_code.
/// The name.
/// The iconb64.
internal async Task CreateGuildFromTemplateAsync(string templateCode, string name, Optional iconb64)
{
var pld = new RestGuildCreateFromTemplatePayload
{
Name = name,
IconBase64 = iconb64
};
var route = $"{Endpoints.GUILDS}{Endpoints.TEMPLATES}/:template_code";
var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {template_code = templateCode }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false);
var json = JObject.Parse(res.Response);
var rawMembers = (JArray)json["members"];
var guild = json.ToDiscordObject();
if (this.Discord is DiscordClient dc)
await dc.OnGuildCreateEventAsync(guild, rawMembers, null).ConfigureAwait(false);
return guild;
}
///
/// Deletes the guild async.
///
/// The guild_id.
internal async Task DeleteGuildAsync(ulong guildId)
{
var route = $"{Endpoints.GUILDS}/:guild_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {guild_id = guildId }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route).ConfigureAwait(false);
if (this.Discord is DiscordClient dc)
{
var gld = dc.GuildsInternal[guildId];
await dc.OnGuildDeleteEventAsync(gld).ConfigureAwait(false);
}
}
///
/// Modifies the guild.
///
/// The guild id.
/// The name.
/// The verification level.
/// The default message notifications.
/// The mfa level.
/// The explicit content filter.
/// The afk channel id.
/// The afk timeout.
/// The iconb64.
/// The owner id.
/// The splashb64.
/// The system channel id.
/// The system channel flags.
/// The public updates channel id.
/// The rules channel id.
/// The description.
/// The banner base64.
/// The discovery base64.
/// The preferred locale.
/// Whether the premium progress bar should be enabled.
/// The reason.
internal async Task ModifyGuildAsync(ulong guildId, Optional name, Optional verificationLevel,
Optional defaultMessageNotifications, Optional mfaLevel,
Optional explicitContentFilter, Optional