diff --git a/DisCatSharp/Clients/DiscordClient.Events.cs b/DisCatSharp/Clients/DiscordClient.Events.cs index 773037149..8bdae9c77 100644 --- a/DisCatSharp/Clients/DiscordClient.Events.cs +++ b/DisCatSharp/Clients/DiscordClient.Events.cs @@ -1,1080 +1,1081 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2023 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 DisCatSharp.Common.Utilities; +using DisCatSharp.Enums; using DisCatSharp.EventArgs; using Microsoft.Extensions.Logging; namespace DisCatSharp; /// /// Represents a discord client. /// public sealed partial class DiscordClient { /// /// Gets the event execution limit. /// internal static TimeSpan EventExecutionLimit { get; } = TimeSpan.FromSeconds(1); #region WebSocket /// /// Fired whenever a WebSocket error occurs within the client. /// public event AsyncEventHandler SocketErrored { add => this._socketErrored.Register(value); remove => this._socketErrored.Unregister(value); } private AsyncEvent _socketErrored; /// /// Fired whenever WebSocket connection is established. /// public event AsyncEventHandler SocketOpened { add => this._socketOpened.Register(value); remove => this._socketOpened.Unregister(value); } private AsyncEvent _socketOpened; /// /// Fired whenever WebSocket connection is terminated. /// public event AsyncEventHandler SocketClosed { add => this._socketClosed.Register(value); remove => this._socketClosed.Unregister(value); } private AsyncEvent _socketClosed; /// /// Fired when the client enters ready state. /// public event AsyncEventHandler Ready { add => this._ready.Register(value); remove => this._ready.Unregister(value); } private AsyncEvent _ready; /// /// Fired whenever a session is resumed. /// public event AsyncEventHandler Resumed { add => this._resumed.Register(value); remove => this._resumed.Unregister(value); } private AsyncEvent _resumed; /// /// Fired on received heartbeat ACK. /// public event AsyncEventHandler Heartbeated { add => this._heartbeated.Register(value); remove => this._heartbeated.Unregister(value); } private AsyncEvent _heartbeated; #endregion #region Channel /// /// Fired when a new channel is created. /// For this Event you need the intent specified in /// public event AsyncEventHandler ChannelCreated { add => this._channelCreated.Register(value); remove => this._channelCreated.Unregister(value); } private AsyncEvent _channelCreated; /// /// Fired when a channel is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler ChannelUpdated { add => this._channelUpdated.Register(value); remove => this._channelUpdated.Unregister(value); } private AsyncEvent _channelUpdated; /// /// Fired when a channel is deleted /// For this Event you need the intent specified in /// public event AsyncEventHandler ChannelDeleted { add => this._channelDeleted.Register(value); remove => this._channelDeleted.Unregister(value); } private AsyncEvent _channelDeleted; /// /// Fired when a dm channel is deleted /// For this Event you need the intent specified in /// public event AsyncEventHandler DmChannelDeleted { add => this._dmChannelDeleted.Register(value); remove => this._dmChannelDeleted.Unregister(value); } private AsyncEvent _dmChannelDeleted; /// /// Fired whenever a channel's pinned message list is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler ChannelPinsUpdated { add => this._channelPinsUpdated.Register(value); remove => this._channelPinsUpdated.Unregister(value); } private AsyncEvent _channelPinsUpdated; #endregion #region Guild /// /// Fired when the user joins a new guild. /// For this Event you need the intent specified in /// /// [alias="GuildJoined"][alias="JoinedGuild"] public event AsyncEventHandler GuildCreated { add => this._guildCreated.Register(value); remove => this._guildCreated.Unregister(value); } private AsyncEvent _guildCreated; /// /// Fired when a guild is becoming available. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildAvailable { add => this._guildAvailable.Register(value); remove => this._guildAvailable.Unregister(value); } private AsyncEvent _guildAvailable; /// /// Fired when a guild is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildUpdated { add => this._guildUpdated.Register(value); remove => this._guildUpdated.Unregister(value); } private AsyncEvent _guildUpdated; /// /// Fired when the user leaves or is removed from a guild. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildDeleted { add => this._guildDeleted.Register(value); remove => this._guildDeleted.Unregister(value); } private AsyncEvent _guildDeleted; /// /// Fired when a guild becomes unavailable. /// public event AsyncEventHandler GuildUnavailable { add => this._guildUnavailable.Register(value); remove => this._guildUnavailable.Unregister(value); } private AsyncEvent _guildUnavailable; /// /// Fired when all guilds finish streaming from Discord. /// public event AsyncEventHandler GuildDownloadCompleted { add => this._guildDownloadCompletedEv.Register(value); remove => this._guildDownloadCompletedEv.Unregister(value); } private AsyncEvent _guildDownloadCompletedEv; /// /// Fired when a guilds emojis get updated /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildEmojisUpdated { add => this._guildEmojisUpdated.Register(value); remove => this._guildEmojisUpdated.Unregister(value); } private AsyncEvent _guildEmojisUpdated; /// /// Fired when a guilds stickers get updated /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildStickersUpdated { add => this._guildStickersUpdated.Register(value); remove => this._guildStickersUpdated.Unregister(value); } private AsyncEvent _guildStickersUpdated; /// /// Fired when a guild integration is updated. /// public event AsyncEventHandler GuildIntegrationsUpdated { add => this._guildIntegrationsUpdated.Register(value); remove => this._guildIntegrationsUpdated.Unregister(value); } private AsyncEvent _guildIntegrationsUpdated; /// /// Fired when a guild audit log entry was created. /// Requires bot to have the permission. /// public event AsyncEventHandler GuildAuditLogEntryCreated { add => this._guildAuditLogEntryCreated.Register(value); remove => this._guildAuditLogEntryCreated.Unregister(value); } private AsyncEvent _guildAuditLogEntryCreated; #endregion #region Automod /// /// Fired when an auto mod rule gets created. /// public event AsyncEventHandler AutomodRuleCreated { add => this._automodRuleCreated.Register(value); remove => this._automodRuleCreated.Unregister(value); } private AsyncEvent _automodRuleCreated; /// /// Fired when an auto mod rule gets updated. /// public event AsyncEventHandler AutomodRuleUpdated { add => this._automodRuleUpdated.Register(value); remove => this._automodRuleUpdated.Unregister(value); } private AsyncEvent _automodRuleUpdated; /// /// Fired when an auto mod rule gets deleted. /// public event AsyncEventHandler AutomodRuleDeleted { add => this._automodRuleDeleted.Register(value); remove => this._automodRuleDeleted.Unregister(value); } private AsyncEvent _automodRuleDeleted; /// /// Fired when a rule is triggered and an action is executed. /// public event AsyncEventHandler AutomodActionExecuted { add => this._automodActionExecuted.Register(value); remove => this._automodActionExecuted.Unregister(value); } private AsyncEvent _automodActionExecuted; #endregion #region Guild Ban /// /// Fired when a guild ban gets added /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildBanAdded { add => this._guildBanAdded.Register(value); remove => this._guildBanAdded.Unregister(value); } private AsyncEvent _guildBanAdded; /// /// Fired when a guild ban gets removed /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildBanRemoved { add => this._guildBanRemoved.Register(value); remove => this._guildBanRemoved.Unregister(value); } private AsyncEvent _guildBanRemoved; #endregion #region Guild Timeout /// /// Fired when a guild member timeout gets added. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildMemberTimeoutAdded { add => this._guildMemberTimeoutAdded.Register(value); remove => this._guildMemberTimeoutAdded.Unregister(value); } private AsyncEvent _guildMemberTimeoutAdded; /// /// Fired when a guild member timeout gets changed. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildMemberTimeoutChanged { add => this._guildMemberTimeoutChanged.Register(value); remove => this._guildMemberTimeoutChanged.Unregister(value); } private AsyncEvent _guildMemberTimeoutChanged; /// /// Fired when a guild member timeout gets removed. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildMemberTimeoutRemoved { add => this._guildMemberTimeoutRemoved.Register(value); remove => this._guildMemberTimeoutRemoved.Unregister(value); } private AsyncEvent _guildMemberTimeoutRemoved; #endregion #region Guild Scheduled Event /// /// Fired when a scheduled Event is created. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildScheduledEventCreated { add => this._guildScheduledEventCreated.Register(value); remove => this._guildScheduledEventCreated.Unregister(value); } private AsyncEvent _guildScheduledEventCreated; /// /// Fired when a scheduled Event is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildScheduledEventUpdated { add => this._guildScheduledEventUpdated.Register(value); remove => this._guildScheduledEventUpdated.Unregister(value); } private AsyncEvent _guildScheduledEventUpdated; /// /// Fired when a scheduled Event is deleted. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildScheduledEventDeleted { add => this._guildScheduledEventDeleted.Register(value); remove => this._guildScheduledEventDeleted.Unregister(value); } private AsyncEvent _guildScheduledEventDeleted; /// /// Fired when a user subscribes to a scheduled event. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildScheduledEventUserAdded { add => this._guildScheduledEventUserAdded.Register(value); remove => this._guildScheduledEventUserAdded.Unregister(value); } private AsyncEvent _guildScheduledEventUserAdded; /// /// Fired when a user unsubscribes from a scheduled event. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildScheduledEventUserRemoved { add => this._guildScheduledEventUserRemoved.Register(value); remove => this._guildScheduledEventUserRemoved.Unregister(value); } private AsyncEvent _guildScheduledEventUserRemoved; #endregion #region Guild Integration /// /// Fired when a guild integration is created. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildIntegrationCreated { add => this._guildIntegrationCreated.Register(value); remove => this._guildIntegrationCreated.Unregister(value); } private AsyncEvent _guildIntegrationCreated; /// /// Fired when a guild integration is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildIntegrationUpdated { add => this._guildIntegrationUpdated.Register(value); remove => this._guildIntegrationUpdated.Unregister(value); } private AsyncEvent _guildIntegrationUpdated; /// /// Fired when a guild integration is deleted. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildIntegrationDeleted { add => this._guildIntegrationDeleted.Register(value); remove => this._guildIntegrationDeleted.Unregister(value); } private AsyncEvent _guildIntegrationDeleted; #endregion #region Guild Member /// /// Fired when a new user joins a guild. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildMemberAdded { add => this._guildMemberAdded.Register(value); remove => this._guildMemberAdded.Unregister(value); } private AsyncEvent _guildMemberAdded; /// /// Fired when a user is removed from a guild (leave/kick/ban). /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildMemberRemoved { add => this._guildMemberRemoved.Register(value); remove => this._guildMemberRemoved.Unregister(value); } private AsyncEvent _guildMemberRemoved; /// /// Fired when a guild member is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildMemberUpdated { add => this._guildMemberUpdated.Register(value); remove => this._guildMemberUpdated.Unregister(value); } private AsyncEvent _guildMemberUpdated; /// /// Fired in response to Gateway Request Guild Members. /// public event AsyncEventHandler GuildMembersChunked { add => this._guildMembersChunked.Register(value); remove => this._guildMembersChunked.Unregister(value); } private AsyncEvent _guildMembersChunked; #endregion #region Guild Role /// /// Fired when a guild role is created. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildRoleCreated { add => this._guildRoleCreated.Register(value); remove => this._guildRoleCreated.Unregister(value); } private AsyncEvent _guildRoleCreated; /// /// Fired when a guild role is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildRoleUpdated { add => this._guildRoleUpdated.Register(value); remove => this._guildRoleUpdated.Unregister(value); } private AsyncEvent _guildRoleUpdated; /// /// Fired when a guild role is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildRoleDeleted { add => this._guildRoleDeleted.Register(value); remove => this._guildRoleDeleted.Unregister(value); } private AsyncEvent _guildRoleDeleted; #endregion #region Invite /// /// Fired when an invite is created. /// For this Event you need the intent specified in /// public event AsyncEventHandler InviteCreated { add => this._inviteCreated.Register(value); remove => this._inviteCreated.Unregister(value); } private AsyncEvent _inviteCreated; /// /// Fired when an invite is deleted. /// For this Event you need the intent specified in /// public event AsyncEventHandler InviteDeleted { add => this._inviteDeleted.Register(value); remove => this._inviteDeleted.Unregister(value); } private AsyncEvent _inviteDeleted; #endregion #region Message /// /// Fired when a message is created. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessageCreated { add => this._messageCreated.Register(value); remove => this._messageCreated.Unregister(value); } private AsyncEvent _messageCreated; /// /// Fired when message is acknowledged by the user. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessageAcknowledged { add => this._messageAcknowledged.Register(value); remove => this._messageAcknowledged.Unregister(value); } private AsyncEvent _messageAcknowledged; /// /// Fired when a message is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessageUpdated { add => this._messageUpdated.Register(value); remove => this._messageUpdated.Unregister(value); } private AsyncEvent _messageUpdated; /// /// Fired when a message is deleted. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessageDeleted { add => this._messageDeleted.Register(value); remove => this._messageDeleted.Unregister(value); } private AsyncEvent _messageDeleted; /// /// Fired when multiple messages are deleted at once. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessagesBulkDeleted { add => this._messagesBulkDeleted.Register(value); remove => this._messagesBulkDeleted.Unregister(value); } private AsyncEvent _messagesBulkDeleted; #endregion #region Message Reaction /// /// Fired when a reaction gets added to a message. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessageReactionAdded { add => this._messageReactionAdded.Register(value); remove => this._messageReactionAdded.Unregister(value); } private AsyncEvent _messageReactionAdded; /// /// Fired when a reaction gets removed from a message. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessageReactionRemoved { add => this._messageReactionRemoved.Register(value); remove => this._messageReactionRemoved.Unregister(value); } private AsyncEvent _messageReactionRemoved; /// /// Fired when all reactions get removed from a message. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessageReactionsCleared { add => this._messageReactionsCleared.Register(value); remove => this._messageReactionsCleared.Unregister(value); } private AsyncEvent _messageReactionsCleared; /// /// Fired when all reactions of a specific reaction are removed from a message. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessageReactionRemovedEmoji { add => this._messageReactionRemovedEmoji.Register(value); remove => this._messageReactionRemovedEmoji.Unregister(value); } private AsyncEvent _messageReactionRemovedEmoji; #endregion #region Activities /// /// Fired when a embedded activity has been updated. /// public event AsyncEventHandler EmbeddedActivityUpdated { add => this._embeddedActivityUpdated.Register(value); remove => this._embeddedActivityUpdated.Unregister(value); } private AsyncEvent _embeddedActivityUpdated; #endregion #region Presence/User Update /// /// Fired when a presence has been updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler PresenceUpdated { add => this._presenceUpdated.Register(value); remove => this._presenceUpdated.Unregister(value); } private AsyncEvent _presenceUpdated; /// /// Fired when the current user updates their settings. /// For this Event you need the intent specified in /// public event AsyncEventHandler UserSettingsUpdated { add => this._userSettingsUpdated.Register(value); remove => this._userSettingsUpdated.Unregister(value); } private AsyncEvent _userSettingsUpdated; /// /// Fired when properties about the current user change. /// /// /// NB: This event only applies for changes to the current user, the client that is connected to Discord. /// For this Event you need the intent specified in /// public event AsyncEventHandler UserUpdated { add => this._userUpdated.Register(value); remove => this._userUpdated.Unregister(value); } private AsyncEvent _userUpdated; #endregion #region Stage Instance /// /// Fired when a Stage Instance is created. /// For this Event you need the intent specified in /// public event AsyncEventHandler StageInstanceCreated { add => this._stageInstanceCreated.Register(value); remove => this._stageInstanceCreated.Unregister(value); } private AsyncEvent _stageInstanceCreated; /// /// Fired when a Stage Instance is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler StageInstanceUpdated { add => this._stageInstanceUpdated.Register(value); remove => this._stageInstanceUpdated.Unregister(value); } private AsyncEvent _stageInstanceUpdated; /// /// Fired when a Stage Instance is deleted. /// For this Event you need the intent specified in /// public event AsyncEventHandler StageInstanceDeleted { add => this._stageInstanceDeleted.Register(value); remove => this._stageInstanceDeleted.Unregister(value); } private AsyncEvent _stageInstanceDeleted; #endregion #region Thread /// /// Fired when a thread is created. /// For this Event you need the intent specified in /// public event AsyncEventHandler ThreadCreated { add => this._threadCreated.Register(value); remove => this._threadCreated.Unregister(value); } private AsyncEvent _threadCreated; /// /// Fired when a thread is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler ThreadUpdated { add => this._threadUpdated.Register(value); remove => this._threadUpdated.Unregister(value); } private AsyncEvent _threadUpdated; /// /// Fired when a thread is deleted. /// For this Event you need the intent specified in /// public event AsyncEventHandler ThreadDeleted { add => this._threadDeleted.Register(value); remove => this._threadDeleted.Unregister(value); } private AsyncEvent _threadDeleted; /// /// Fired when a thread member is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler ThreadListSynced { add => this._threadListSynced.Register(value); remove => this._threadListSynced.Unregister(value); } private AsyncEvent _threadListSynced; /// /// Fired when a thread member is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler ThreadMemberUpdated { add => this._threadMemberUpdated.Register(value); remove => this._threadMemberUpdated.Unregister(value); } private AsyncEvent _threadMemberUpdated; /// /// Fired when the thread members are updated. /// For this Event you need the or intent specified in /// public event AsyncEventHandler ThreadMembersUpdated { add => this._threadMembersUpdated.Register(value); remove => this._threadMembersUpdated.Unregister(value); } private AsyncEvent _threadMembersUpdated; #endregion #region Voice /// /// Fired when someone joins/leaves/moves voice channels. /// For this Event you need the intent specified in /// public event AsyncEventHandler VoiceStateUpdated { add => this._voiceStateUpdated.Register(value); remove => this._voiceStateUpdated.Unregister(value); } private AsyncEvent _voiceStateUpdated; /// /// Fired when a guild's voice server is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler VoiceServerUpdated { add => this._voiceServerUpdated.Register(value); remove => this._voiceServerUpdated.Unregister(value); } private AsyncEvent _voiceServerUpdated; #endregion #region Application /// /// Fired when a new application command is registered. /// public event AsyncEventHandler ApplicationCommandCreated { add => this._applicationCommandCreated.Register(value); remove => this._applicationCommandCreated.Unregister(value); } private AsyncEvent _applicationCommandCreated; /// /// Fired when an application command is updated. /// public event AsyncEventHandler ApplicationCommandUpdated { add => this._applicationCommandUpdated.Register(value); remove => this._applicationCommandUpdated.Unregister(value); } private AsyncEvent _applicationCommandUpdated; /// /// Fired when an application command is deleted. /// public event AsyncEventHandler ApplicationCommandDeleted { add => this._applicationCommandDeleted.Register(value); remove => this._applicationCommandDeleted.Unregister(value); } private AsyncEvent _applicationCommandDeleted; /// /// Fired when a new application command is registered. /// public event AsyncEventHandler GuildApplicationCommandCountUpdated { add => this._guildApplicationCommandCountUpdated.Register(value); remove => this._guildApplicationCommandCountUpdated.Unregister(value); } private AsyncEvent _guildApplicationCommandCountUpdated; /// /// Fired when a user uses a context menu. /// public event AsyncEventHandler ContextMenuInteractionCreated { add => this._contextMenuInteractionCreated.Register(value); remove => this._contextMenuInteractionCreated.Unregister(value); } private AsyncEvent _contextMenuInteractionCreated; /// /// Fired when application command permissions gets updated. /// public event AsyncEventHandler ApplicationCommandPermissionsUpdated { add => this._applicationCommandPermissionsUpdated.Register(value); remove => this._applicationCommandPermissionsUpdated.Unregister(value); } private AsyncEvent _applicationCommandPermissionsUpdated; #endregion #region Misc /// /// Fired when an interaction is invoked. /// public event AsyncEventHandler InteractionCreated { add => this._interactionCreated.Register(value); remove => this._interactionCreated.Unregister(value); } private AsyncEvent _interactionCreated; /// /// Fired when a component is invoked. /// public event AsyncEventHandler ComponentInteractionCreated { add => this._componentInteractionCreated.Register(value); remove => this._componentInteractionCreated.Unregister(value); } private AsyncEvent _componentInteractionCreated; /// /// Fired when a user starts typing in a channel. /// public event AsyncEventHandler TypingStarted { add => this._typingStarted.Register(value); remove => this._typingStarted.Unregister(value); } private AsyncEvent _typingStarted; /// /// Fired when an unknown event gets received. /// public event AsyncEventHandler UnknownEvent { add => this._unknownEvent.Register(value); remove => this._unknownEvent.Unregister(value); } private AsyncEvent _unknownEvent; /// /// Fired whenever webhooks update. /// public event AsyncEventHandler WebhooksUpdated { add => this._webhooksUpdated.Register(value); remove => this._webhooksUpdated.Unregister(value); } private AsyncEvent _webhooksUpdated; /// /// Fired whenever an error occurs within an event handler. /// public event AsyncEventHandler ClientErrored { add => this._clientErrored.Register(value); remove => this._clientErrored.Unregister(value); } private AsyncEvent _clientErrored; #endregion #region Error Handling /// /// Handles event errors. /// /// The event. /// The exception. /// The event handler. /// The sender. /// The event args. internal void EventErrorHandler(AsyncEvent asyncEvent, Exception ex, AsyncEventHandler handler, TSender sender, TArgs eventArgs) where TArgs : AsyncEventArgs { if (ex is AsyncEventTimeoutException) { this.Logger.LogWarning(LoggerEvents.EventHandlerException, $"An event handler for {asyncEvent.Name} took too long to execute. Defined as \"{handler.Method.ToString().Replace(handler.Method.ReturnType.ToString(), "").TrimStart()}\" located in \"{handler.Method.DeclaringType}\"."); return; } this.Logger.LogError(LoggerEvents.EventHandlerException, ex, "Event handler exception for event {0} thrown from {1} (defined in {2})", asyncEvent.Name, handler.Method, handler.Method.DeclaringType); this._clientErrored.InvokeAsync(this, new ClientErrorEventArgs(this.ServiceProvider) { EventName = asyncEvent.Name, Exception = ex }).ConfigureAwait(false).GetAwaiter().GetResult(); } /// /// Fired when a ratelimit was hit. /// public event AsyncEventHandler RateLimitHit { add => this._rateLimitHit.Register(value); remove => this._rateLimitHit.Unregister(value); } internal AsyncEvent _rateLimitHit; /// /// Fired on heartbeat attempt cancellation due to too many failed heartbeats. /// public event AsyncEventHandler Zombied { add => this._zombied.Register(value); remove => this._zombied.Unregister(value); } private AsyncEvent _zombied; /// /// Fired when a gateway payload is received. /// public event AsyncEventHandler PayloadReceived { add => this._payloadReceived.Register(value); remove => this._payloadReceived.Unregister(value); } private AsyncEvent _payloadReceived; /// /// Handles event handler exceptions. /// /// The event. /// The exception. /// The event handler. /// The sender. /// The event args. private void Goof(AsyncEvent asyncEvent, Exception ex, AsyncEventHandler handler, TSender sender, TArgs eventArgs) where TArgs : AsyncEventArgs => this.Logger.LogCritical(LoggerEvents.EventHandlerException, ex, "Exception event handler {0} (defined in {1}) threw an exception", handler.Method, handler.Method.DeclaringType); #endregion } diff --git a/DisCatSharp/Entities/Channel/DiscordChannel.cs b/DisCatSharp/Entities/Channel/DiscordChannel.cs index ddd496382..9f2b7aa59 100644 --- a/DisCatSharp/Entities/Channel/DiscordChannel.cs +++ b/DisCatSharp/Entities/Channel/DiscordChannel.cs @@ -1,1478 +1,1471 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2023 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.Attributes; 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 or tags. /// /// 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)); 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. #pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. public async Task TryGetMessageAsync(ulong id, bool fetch = true) #pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. { 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 mdl.AvailableTags.HasValue && mdl.AvailableTags.Value.Count > 20 ? throw new NotSupportedException("Cannot have more than 20 tags in a forum channel.") : (Task)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 - - [Obsolete, DiscordDeprecated("Stage privacy level is removed")] - 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, null, reason); - /// /// Opens a stage. /// /// Topic of the stage. /// Whether @everyone should be notified. /// The associated scheduled event id. /// 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, ulong? scheduledEventId = null, string reason = null) => await this.Discord.ApiClient.CreateStageInstanceAsync(this.Id, topic, sendStartNotification, scheduledEventId, 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. - [DiscordDeprecated("Privacy level will be removed in next versions"), Obsolete] - public async Task ModifyStageAsync(Optional topic, Optional privacyLevel, string reason = null) + public async Task ModifyStageAsync(Optional topic, string reason = null) => await this.Discord.ApiClient.ModifyStageInstanceAsync(this.Id, topic, 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. #pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. public async Task CreatePostAsync(string name, DiscordMessageBuilder builder, int? rateLimitPerUser = null, IEnumerable? tags = null, string reason = null) #pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. => 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. #pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. public ForumPostTag? TryGetForumPostTag(ulong id) #pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. { 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") : this.AvailableTags.Count == 20 ? throw new NotSupportedException("Cannot have more than 20 tags in a forum channel.") : 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, default, member.MemberFlags, 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/Guild/ThreadAndForum/DiscordThreadChannel.cs b/DisCatSharp/Entities/Guild/ThreadAndForum/DiscordThreadChannel.cs index 77885d5c5..ceb645224 100644 --- a/DisCatSharp/Entities/Guild/ThreadAndForum/DiscordThreadChannel.cs +++ b/DisCatSharp/Entities/Guild/ThreadAndForum/DiscordThreadChannel.cs @@ -1,388 +1,412 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2023 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.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using DisCatSharp.Attributes; using DisCatSharp.Enums; using DisCatSharp.Exceptions; using DisCatSharp.Net.Models; using DisCatSharp.Net.Serialization; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a discord thread channel. /// public class DiscordThreadChannel : DiscordChannel { /// /// Gets ID of the owner that started this thread. /// [JsonProperty("owner_id", NullValueHandling = NullValueHandling.Ignore)] public ulong OwnerId { get; internal set; } [JsonProperty("total_message_sent", DefaultValueHandling = DefaultValueHandling.Ignore)] public int TotalMessagesSent { get; internal set; } /// /// Gets an approximate count of messages in a thread, stops counting at 50. /// [JsonProperty("message_count", NullValueHandling = NullValueHandling.Ignore)] public int? MessageCount { get; internal set; } /// /// Gets an approximate count of users in a thread, stops counting at 50. /// [JsonProperty("member_count", NullValueHandling = NullValueHandling.Ignore)] public int? MemberCount { get; internal set; } /// /// Represents the current member for this thread. This will have a value if the user has joined the thread. /// [JsonProperty("member", NullValueHandling = NullValueHandling.Ignore)] public DiscordThreadChannelMember CurrentMember { get; internal set; } /// /// Gets the threads metadata. /// [JsonProperty("thread_metadata", NullValueHandling = NullValueHandling.Ignore)] public DiscordThreadChannelMetadata ThreadMetadata { get; internal set; } /// /// Gets the thread members object. /// [JsonIgnore] public IReadOnlyDictionary ThreadMembers => new ReadOnlyConcurrentDictionary(this.ThreadMembersInternal); [JsonProperty("thread_member", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary ThreadMembersInternal; /// /// List of applied tag ids. /// [JsonIgnore] internal IReadOnlyList AppliedTagIds => this.AppliedTagIdsInternal; /// /// List of applied tag ids. /// [JsonProperty("applied_tags", NullValueHandling = NullValueHandling.Ignore)] internal List AppliedTagIdsInternal; /// /// Gets the list of applied tags. /// Only applicable for forum channel posts. /// [JsonIgnore] public IReadOnlyList AppliedTags => this.AppliedTagIds?.Select(id => this.Parent.GetForumPostTag(id)).Where(x => x != null).ToList(); /// /// Initializes a new instance of the class. /// internal DiscordThreadChannel() { } #region Methods /// /// Modifies the current thread. /// /// Action to perform on this thread /// Thrown when the client does not have the permission. /// Thrown when the thread does not exist. /// 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 . public Task ModifyAsync(Action action) { var mdl = new ThreadEditModel(); action(mdl); return this.Parent.Type == ChannelType.Forum && mdl.AppliedTags.HasValue && mdl.AppliedTags.Value.Count() > 5 ? throw new NotSupportedException("Cannot have more than 5 applied tags.") : this.Discord.ApiClient.ModifyThreadAsync(this.Id, this.Parent.Type, mdl.Name, mdl.Locked, mdl.Archived, mdl.PerUserRateLimit, mdl.AutoArchiveDuration, mdl.Invitable, mdl.AppliedTags, mdl.Pinned, mdl.AuditLogReason); } /// /// Add a tag to the current thread. /// /// The tag to add. /// The reason for the audit logs. /// Thrown when the client does not have the permission. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task AddTagAsync(ForumPostTag tag, string reason = null) => this.AppliedTagIds.Count == 5 ? throw new NotSupportedException("Cannot have more than 5 applied tags.") : this.Discord.ApiClient.ModifyThreadAsync(this.Id, this.Parent.Type, null, null, null, null, null, null, new List(this.AppliedTags) { tag }, null, reason); /// /// Remove a tag from the current thread. /// /// The tag to remove. /// The reason for the audit logs. /// Thrown when the client does not have the permission. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task RemoveTagAsync(ForumPostTag tag, string reason = null) => await this.Discord.ApiClient.ModifyThreadAsync(this.Id, this.Parent.Type, null, null, null, null, null, null, new List(this.AppliedTags).Where(x => x != tag).ToList(), null, reason); /// /// Archives a thread. /// /// Whether the thread should be locked. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task ArchiveAsync(bool locked = true, string reason = null) - => this.Discord.ApiClient.ModifyThreadAsync(this.Id, this.Parent.Type, null, locked, true, null, null, null, null, null, reason); + [DiscordDeprecated("The locked parameter will be removed soon and is not applied anymore.")] + public Task ArchiveAsync([DiscordDeprecated("The locked parameter will be removed soon and is not applied anymore.")] bool locked = true, string reason = null) + => this.Discord.ApiClient.ModifyThreadAsync(this.Id, this.Parent.Type, null, null, true, null, null, null, null, null, reason); /// /// Unarchives a thread. /// /// Reason for audit logs. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task UnarchiveAsync(string reason = null) - => this.Discord.ApiClient.ModifyThreadAsync(this.Id, this.Parent.Type, null, null, false, null, null, null, null, null, reason); + => this.Discord.ApiClient.ModifyThreadAsync(this.Id, this.Parent.Type, null, false, null, null, null, null, null, null, reason); + + /// + /// Locks a thread. + /// + /// Reason for audit logs. + /// Thrown when the client does not have the permission. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public Task LockAsync(string reason = null) + => this.Discord.ApiClient.ModifyThreadAsync(this.Id, this.Parent.Type, null, true, null, null, null, null, null, null, reason); + + /// + /// Unlocks a thread. + /// + /// Reason for audit logs. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public Task UnlockAsync(string reason = null) + => this.Discord.ApiClient.ModifyThreadAsync(this.Id, this.Parent.Type, null, false, true, null, null, null, null, null, reason); + /// /// Gets the members of a thread. Needs the intent. /// /// Whether to request the member object. (If set to true, will paginate the result) /// Request all members after the specified. (Currently only utilized if withMember is set to true) /// The amount of members to fetch. (Currently only utilized if withMember is set to true) /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task> GetMembersAsync(bool withMember = false, ulong? after = null, int? limit = null) => await this.Discord.ApiClient.GetThreadMembersAsync(this.Id); /// /// Adds a member to this thread. /// /// The member id to be added. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task AddMemberAsync(ulong memberId) => this.Discord.ApiClient.AddThreadMemberAsync(this.Id, memberId); /// /// Adds a member to this thread. /// /// The member to be added. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task AddMemberAsync(DiscordMember member) => this.AddMemberAsync(member.Id); /// /// Gets a member in this thread. /// /// The id of the member to get. /// Whether to request the member object. /// Thrown when the member is not part of the thread. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetMemberAsync(ulong memberId, bool withMember = false) => this.Discord.ApiClient.GetThreadMemberAsync(this.Id, memberId, withMember); /// /// Tries to get a member in this thread. /// /// The id of the member to get. /// Whether to request the member object. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. #pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. public async Task TryGetMemberAsync(ulong memberId, bool withMember = false) #pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. { try { return await this.GetMemberAsync(memberId, withMember).ConfigureAwait(false); } catch (NotFoundException) { return null; } } /// /// Gets a member in this thread. /// /// The member to get. /// Whether to request the member object. /// Thrown when the member is not part of the thread. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetMemberAsync(DiscordMember member, bool withMember = false) => this.Discord.ApiClient.GetThreadMemberAsync(this.Id, member.Id, withMember); /// /// Tries to get a member in this thread. /// /// The member to get. /// Whether to request the member object. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. #pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. public async Task TryGetMemberAsync(DiscordMember member, bool withMember = false) #pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. { try { return await this.GetMemberAsync(member, withMember).ConfigureAwait(false); } catch (NotFoundException) { return null; } } /// /// Removes a member from this thread. /// /// The member id to be removed. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RemoveMemberAsync(ulong memberId) => this.Discord.ApiClient.RemoveThreadMemberAsync(this.Id, memberId); /// /// Removes a member from this thread. Only applicable to private threads. /// /// The member to be removed. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RemoveMemberAsync(DiscordMember member) => this.RemoveMemberAsync(member.Id); /// /// Adds a role to this thread. Only applicable to private threads. /// /// The role id to be added. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task AddRoleAsync(ulong roleId) { var role = this.Guild.GetRole(roleId); var members = await this.Guild.GetAllMembersAsync(); var roleMembers = members.Where(m => m.Roles.Contains(role)); foreach (var member in roleMembers) { await this.Discord.ApiClient.AddThreadMemberAsync(this.Id, member.Id); } } /// /// Adds a role to this thread. Only applicable to private threads. /// /// The role to be added. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task AddRoleAsync(DiscordRole role) => this.AddRoleAsync(role.Id); /// /// Removes a role from this thread. Only applicable to private threads. /// /// The role id to be removed. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task RemoveRoleAsync(ulong roleId) { var role = this.Guild.GetRole(roleId); var members = await this.Guild.GetAllMembersAsync(); var roleMembers = members.Where(m => m.Roles.Contains(role)); foreach (var member in roleMembers) { await this.Discord.ApiClient.RemoveThreadMemberAsync(this.Id, member.Id); } } /// /// Removes a role from this thread. Only applicable to private threads. /// /// The role to be removed. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RemoveRoleAsync(DiscordRole role) => this.RemoveRoleAsync(role.Id); /// /// Joins a thread. /// /// Thrown when the client has no access to this thread. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task JoinAsync() => this.Discord.ApiClient.JoinThreadAsync(this.Id); /// /// Leaves a thread. /// /// Thrown when the client has no access to this thread. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task LeaveAsync() => this.Discord.ApiClient.LeaveThreadAsync(this.Id); /// /// Returns a string representation of this thread. /// /// String representation of this thread. public override string ToString() => this.Type switch { ChannelType.NewsThread => $"News thread {this.Name} ({this.Id})", ChannelType.PublicThread => $"Thread {this.Name} ({this.Id})", ChannelType.PrivateThread => $"Private thread {this.Name} ({this.Id})", _ => $"Thread {this.Name} ({this.Id})", }; #endregion }