diff --git a/DisCatSharp/Clients/BaseDiscordClient.cs b/DisCatSharp/Clients/BaseDiscordClient.cs
index 23d45ded0..3d3401660 100644
--- a/DisCatSharp/Clients/BaseDiscordClient.cs
+++ b/DisCatSharp/Clients/BaseDiscordClient.cs
@@ -1,355 +1,353 @@
// 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.
#pragma warning disable CS0618
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.Net;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
-using Newtonsoft.Json;
-
namespace DisCatSharp;
///
/// Represents a common base for various Discord Client implementations.
///
public abstract class BaseDiscordClient : IDisposable
{
///
/// Gets the api client.
///
internal protected DiscordApiClient ApiClient { get; }
///
/// Gets the configuration.
///
internal protected DiscordConfiguration Configuration { get; }
///
/// Gets the instance of the logger for this client.
///
public ILogger Logger { get; internal set; }
///
/// Gets the string representing the version of bot lib.
///
public string VersionString { get; }
///
/// Gets the bot library name.
///
public string BotLibrary { get; }
[Obsolete("Use GetLibraryDeveloperTeamAsync")]
public DisCatSharpTeam LibraryDeveloperTeamAsync
=> this.GetLibraryDevelopmentTeamAsync().Result;
///
/// Gets the current user.
///
public DiscordUser CurrentUser { get; internal set; }
///
/// Gets the current application.
///
public DiscordApplication CurrentApplication { get; internal set; }
///
/// Exposes a Http Client for custom operations.
///
public HttpClient RestClient { get; internal set; }
///
/// Gets the cached guilds for this client.
///
public abstract IReadOnlyDictionary Guilds { get; }
///
/// Gets the cached users for this client.
///
public ConcurrentDictionary UserCache { get; internal set; }
///
/// Gets the service provider.
/// This allows passing data around without resorting to static members.
/// Defaults to null.
///
internal IServiceProvider ServiceProvider { get; set; }
///
/// Gets the list of available voice regions. Note that this property will not contain VIP voice regions.
///
public IReadOnlyDictionary VoiceRegions
=> this.VoiceRegionsLazy.Value;
///
/// Gets the list of available voice regions. This property is meant as a way to modify .
///
protected internal ConcurrentDictionary InternalVoiceRegions { get; set; }
internal Lazy> VoiceRegionsLazy;
///
/// Initializes this Discord API client.
///
/// Configuration for this client.
protected BaseDiscordClient(DiscordConfiguration config)
{
this.Configuration = new DiscordConfiguration(config);
this.ServiceProvider = config.ServiceProvider;
if (this.ServiceProvider != null)
{
this.Configuration.LoggerFactory ??= config.ServiceProvider.GetService();
this.Logger = config.ServiceProvider.GetService>();
}
if (this.Configuration.LoggerFactory == null)
{
this.Configuration.LoggerFactory = new DefaultLoggerFactory();
this.Configuration.LoggerFactory.AddProvider(new DefaultLoggerProvider(this));
}
this.Logger ??= this.Configuration.LoggerFactory.CreateLogger();
this.ApiClient = new DiscordApiClient(this);
this.UserCache = new ConcurrentDictionary();
this.InternalVoiceRegions = new ConcurrentDictionary();
this.VoiceRegionsLazy = new Lazy>(() => new ReadOnlyDictionary(this.InternalVoiceRegions));
this.RestClient = new();
this.RestClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Utilities.GetUserAgent());
this.RestClient.DefaultRequestHeaders.TryAddWithoutValidation("X-Discord-Locale", this.Configuration.Locale);
var a = typeof(DiscordClient).GetTypeInfo().Assembly;
var iv = a.GetCustomAttribute();
if (iv != null)
{
this.VersionString = iv.InformationalVersion;
}
else
{
var v = a.GetName().Version;
var vs = v.ToString(3);
if (v.Revision > 0)
this.VersionString = $"{vs}, CI build {v.Revision}";
}
this.BotLibrary = "DisCatSharp";
}
///
/// Gets the current API application.
///
public async Task GetCurrentApplicationAsync()
{
var tapp = await this.ApiClient.GetCurrentApplicationOauth2InfoAsync().ConfigureAwait(false);
var app = new DiscordApplication
{
Discord = this,
Id = tapp.Id,
Name = tapp.Name,
Description = tapp.Description,
Summary = tapp.Summary,
IconHash = tapp.IconHash,
RpcOrigins = tapp.RpcOrigins != null ? new ReadOnlyCollection(tapp.RpcOrigins) : null,
Flags = tapp.Flags,
IsHook = tapp.IsHook,
Type = tapp.Type,
PrivacyPolicyUrl = tapp.PrivacyPolicyUrl,
TermsOfServiceUrl = tapp.TermsOfServiceUrl,
CustomInstallUrl = tapp.CustomInstallUrl,
InstallParams = tapp.InstallParams,
RoleConnectionsVerificationUrl = tapp.RoleConnectionsVerificationUrl,
Tags = (tapp.Tags ?? Enumerable.Empty()).ToArray()
};
if (tapp.Team == null)
{
app.Owners = new List(new[] { new DiscordUser(tapp.Owner) });
app.Team = null;
app.TeamName = null;
}
else
{
app.Team = new DiscordTeam(tapp.Team);
var members = tapp.Team.Members
.Select(x => new DiscordTeamMember(x) { TeamId = app.Team.Id, TeamName = app.Team.Name, User = new DiscordUser(x.User) })
.ToArray();
var owners = members
.Where(x => x.MembershipStatus == DiscordTeamMembershipStatus.Accepted)
.Select(x => x.User)
.ToArray();
app.Owners = new List(owners);
app.Team.Owner = owners.FirstOrDefault(x => x.Id == tapp.Team.OwnerId);
app.Team.Members = new List(members);
app.TeamName = app.Team.Name;
}
app.GuildId = tapp.GuildId.ValueOrDefault();
app.Slug = tapp.Slug.ValueOrDefault();
app.PrimarySkuId = tapp.PrimarySkuId.ValueOrDefault();
app.VerifyKey = tapp.VerifyKey.ValueOrDefault();
app.CoverImageHash = tapp.CoverImageHash.ValueOrDefault();
app.Guild = tapp.Guild.ValueOrDefault();
app.ApproximateGuildCount = tapp.ApproximateGuildCount.ValueOrDefault();
app.RequiresCodeGrant = tapp.BotRequiresCodeGrant.ValueOrDefault();
app.IsPublic = tapp.IsPublicBot.ValueOrDefault();
app.RedirectUris = tapp.RedirectUris.ValueOrDefault();
app.InteractionsEndpointUrl = tapp.InteractionsEndpointUrl.ValueOrDefault();
return app;
}
///
/// Updates the current API application.
///
/// The new description.
/// The new interactions endpoint url.
/// The new role connections verification url.
/// The new tags.
/// The new application icon.
/// The updated application.
public async Task UpdateCurrentApplicationInfoAsync(Optional description, Optional interactionsEndpointUrl, Optional roleConnectionsVerificationUrl, Optional?> tags, Optional icon)
{
var iconb64 = ImageTool.Base64FromStream(icon);
if (tags != null && tags.HasValue && tags.Value != null)
if (tags.Value.Any(x => x.Length > 20))
throw new InvalidOperationException("Tags can not exceed 20 chars.");
_ = await this.ApiClient.ModifyCurrentApplicationInfoAsync(description, interactionsEndpointUrl, roleConnectionsVerificationUrl, tags, iconb64);
// We use GetCurrentApplicationAsync because modify returns internal data not meant for developers.
var app = await this.GetCurrentApplicationAsync();
this.CurrentApplication = app;
return app;
}
#pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved
///
/// Gets a list of voice regions.
///
/// Thrown when Discord is unable to process the request.
public Task> ListVoiceRegionsAsync()
#pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved
=> this.ApiClient.ListVoiceRegionsAsync();
///
/// Initializes this client. This method fetches information about current user, application, and voice regions.
///
public virtual async Task InitializeAsync()
{
if (this.CurrentUser == null)
{
this.CurrentUser = await this.ApiClient.GetCurrentUserAsync().ConfigureAwait(false);
this.UserCache.AddOrUpdate(this.CurrentUser.Id, this.CurrentUser, (id, xu) => this.CurrentUser);
}
if (this.Configuration.TokenType == TokenType.Bot && this.CurrentApplication == null)
this.CurrentApplication = await this.GetCurrentApplicationAsync().ConfigureAwait(false);
if (this.Configuration.TokenType != TokenType.Bearer && this.InternalVoiceRegions.IsEmpty)
{
var vrs = await this.ListVoiceRegionsAsync().ConfigureAwait(false);
foreach (var xvr in vrs)
this.InternalVoiceRegions.TryAdd(xvr.Id, xvr);
}
}
///
/// Gets the current gateway info for the provided token.
/// If no value is provided, the configuration value will be used instead.
///
/// A gateway info object.
public async Task GetGatewayInfoAsync(string token = null)
{
if (this.Configuration.TokenType != TokenType.Bot)
throw new InvalidOperationException("Only bot tokens can access this info.");
if (string.IsNullOrEmpty(this.Configuration.Token))
{
if (string.IsNullOrEmpty(token))
throw new InvalidOperationException("Could not locate a valid token.");
this.Configuration.Token = token;
var res = await this.ApiClient.GetGatewayInfoAsync().ConfigureAwait(false);
this.Configuration.Token = null;
return res;
}
return await this.ApiClient.GetGatewayInfoAsync().ConfigureAwait(false);
}
///
/// Gets some information about the development team behind DisCatSharp.
/// Can be used for crediting etc.
/// Note: This call contacts servers managed by the DCS team, no information is collected.
/// The team, or null with errors being logged on failure.
///
[Obsolete("Don't use this right now, inactive")]
public async Task GetLibraryDevelopmentTeamAsync()
=> await DisCatSharpTeam.Get(this.RestClient, this.Logger, this.ApiClient).ConfigureAwait(false);
///
/// Gets a cached user.
///
/// The user id.
internal DiscordUser GetCachedOrEmptyUserInternal(ulong userId)
{
this.TryGetCachedUserInternal(userId, out var user);
return user;
}
///
/// Tries the get a cached user.
///
/// The user id.
/// The user.
internal bool TryGetCachedUserInternal(ulong userId, out DiscordUser user)
{
if (this.UserCache.TryGetValue(userId, out user))
return true;
user = new DiscordUser { Id = userId, Discord = this };
return false;
}
///
/// Disposes this client.
///
public abstract void Dispose();
}
diff --git a/DisCatSharp/Clients/DiscordClient.cs b/DisCatSharp/Clients/DiscordClient.cs
index c13cc8c92..fa8210222 100644
--- a/DisCatSharp/Clients/DiscordClient.cs
+++ b/DisCatSharp/Clients/DiscordClient.cs
@@ -1,1654 +1,1652 @@
// 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.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DisCatSharp.Common.Utilities;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.EventArgs;
using DisCatSharp.Exceptions;
using DisCatSharp.Net;
using DisCatSharp.Net.Abstractions;
using DisCatSharp.Net.Models;
using DisCatSharp.Net.Serialization;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
namespace DisCatSharp;
///
/// A Discord API wrapper.
///
public sealed partial class DiscordClient : BaseDiscordClient
{
#region Internal Fields/Properties
internal bool IsShard = false;
///
/// Gets the message cache.
///
internal RingBuffer MessageCache { get; }
private List _extensions = new();
private StatusUpdate _status;
///
/// Gets the connection lock.
///
private readonly ManualResetEventSlim _connectionLock = new(true);
#endregion
#region Public Fields/Properties
///
/// Gets the gateway protocol version.
///
public int GatewayVersion { get; internal set; }
///
/// Gets the gateway session information for this client.
///
public GatewayInfo GatewayInfo { get; internal set; }
///
/// Gets the gateway URL.
///
public Uri GatewayUri { get; internal set; }
///
/// Gets the total number of shards the bot is connected to.
///
public int ShardCount => this.GatewayInfo != null
? this.GatewayInfo.ShardCount
: this.Configuration.ShardCount;
///
/// Gets the currently connected shard ID.
///
public int ShardId
=> this.Configuration.ShardId;
///
/// Gets the intents configured for this client.
///
public DiscordIntents Intents
=> this.Configuration.Intents;
///
/// Gets a dictionary of guilds that this client is in. The dictionary's key is the guild ID. Note that the
/// guild objects in this dictionary will not be filled in if the specific guilds aren't available (the
/// or events haven't been fired yet)
///
public override IReadOnlyDictionary Guilds { get; }
internal ConcurrentDictionary GuildsInternal = new();
///
/// Gets the websocket latency for this client.
///
public int Ping
=> Volatile.Read(ref this._ping);
private int _ping;
///
/// Gets the collection of presences held by this client.
///
public IReadOnlyDictionary Presences
=> this._presencesLazy.Value;
internal Dictionary PresencesInternal = new();
private Lazy> _presencesLazy;
///
/// Gets the collection of presences held by this client.
///
public IReadOnlyDictionary EmbeddedActivities
=> this._embeddedActivitiesLazy.Value;
internal Dictionary EmbeddedActivitiesInternal = new();
private Lazy> _embeddedActivitiesLazy;
#endregion
#region Constructor/Internal Setup
///
/// Initializes a new instance of .
///
/// Specifies configuration parameters.
public DiscordClient(DiscordConfiguration config)
: base(config)
{
if (this.Configuration.MessageCacheSize > 0)
{
var intents = this.Configuration.Intents;
this.MessageCache = intents.HasIntent(DiscordIntents.GuildMessages) || intents.HasIntent(DiscordIntents.DirectMessages)
? new RingBuffer(this.Configuration.MessageCacheSize)
: null;
}
this.InternalSetup();
this.Guilds = new ReadOnlyConcurrentDictionary(this.GuildsInternal);
}
///
/// Internal setup of the Client.
///
internal void InternalSetup()
{
this._clientErrored = new AsyncEvent("CLIENT_ERRORED", EventExecutionLimit, this.Goof);
this._socketErrored = new AsyncEvent("SOCKET_ERRORED", EventExecutionLimit, this.Goof);
this._socketOpened = new AsyncEvent("SOCKET_OPENED", EventExecutionLimit, this.EventErrorHandler);
this._socketClosed = new AsyncEvent("SOCKET_CLOSED", EventExecutionLimit, this.EventErrorHandler);
this._ready = new AsyncEvent("READY", EventExecutionLimit, this.EventErrorHandler);
this._resumed = new AsyncEvent("RESUMED", EventExecutionLimit, this.EventErrorHandler);
this._channelCreated = new AsyncEvent("CHANNEL_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._channelUpdated = new AsyncEvent("CHANNEL_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._channelDeleted = new AsyncEvent("CHANNEL_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._dmChannelDeleted = new AsyncEvent("DM_CHANNEL_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._channelPinsUpdated = new AsyncEvent("CHANNEL_PINS_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildCreated = new AsyncEvent("GUILD_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._guildAvailable = new AsyncEvent("GUILD_AVAILABLE", EventExecutionLimit, this.EventErrorHandler);
this._guildUpdated = new AsyncEvent("GUILD_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildDeleted = new AsyncEvent("GUILD_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._guildUnavailable = new AsyncEvent("GUILD_UNAVAILABLE", EventExecutionLimit, this.EventErrorHandler);
this._guildDownloadCompletedEv = new AsyncEvent("GUILD_DOWNLOAD_COMPLETED", EventExecutionLimit, this.EventErrorHandler);
this._inviteCreated = new AsyncEvent("INVITE_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._inviteDeleted = new AsyncEvent("INVITE_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._messageCreated = new AsyncEvent("MESSAGE_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._presenceUpdated = new AsyncEvent("PRESENCE_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildBanAdded = new AsyncEvent("GUILD_BAN_ADD", EventExecutionLimit, this.EventErrorHandler);
this._guildBanRemoved = new AsyncEvent("GUILD_BAN_REMOVED", EventExecutionLimit, this.EventErrorHandler);
this._guildEmojisUpdated = new AsyncEvent("GUILD_EMOJI_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildStickersUpdated = new AsyncEvent("GUILD_STICKER_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildIntegrationsUpdated = new AsyncEvent("GUILD_INTEGRATIONS_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildMemberAdded = new AsyncEvent("GUILD_MEMBER_ADD", EventExecutionLimit, this.EventErrorHandler);
this._guildMemberRemoved = new AsyncEvent("GUILD_MEMBER_REMOVED", EventExecutionLimit, this.EventErrorHandler);
this._guildMemberUpdated = new AsyncEvent("GUILD_MEMBER_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildRoleCreated = new AsyncEvent("GUILD_ROLE_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._guildRoleUpdated = new AsyncEvent("GUILD_ROLE_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildRoleDeleted = new AsyncEvent("GUILD_ROLE_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._messageAcknowledged = new AsyncEvent("MESSAGE_ACKNOWLEDGED", EventExecutionLimit, this.EventErrorHandler);
this._messageUpdated = new AsyncEvent("MESSAGE_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._messageDeleted = new AsyncEvent("MESSAGE_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._messagesBulkDeleted = new AsyncEvent("MESSAGE_BULK_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._interactionCreated = new AsyncEvent("INTERACTION_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._componentInteractionCreated = new AsyncEvent("COMPONENT_INTERACTED", EventExecutionLimit, this.EventErrorHandler);
this._contextMenuInteractionCreated = new AsyncEvent("CONTEXT_MENU_INTERACTED", EventExecutionLimit, this.EventErrorHandler);
this._typingStarted = new AsyncEvent("TYPING_STARTED", EventExecutionLimit, this.EventErrorHandler);
this._userSettingsUpdated = new AsyncEvent("USER_SETTINGS_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._userUpdated = new AsyncEvent("USER_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._voiceStateUpdated = new AsyncEvent("VOICE_STATE_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._voiceServerUpdated = new AsyncEvent("VOICE_SERVER_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildMembersChunked = new AsyncEvent("GUILD_MEMBERS_CHUNKED", EventExecutionLimit, this.EventErrorHandler);
this._unknownEvent = new AsyncEvent("UNKNOWN_EVENT", EventExecutionLimit, this.EventErrorHandler);
this._messageReactionAdded = new AsyncEvent("MESSAGE_REACTION_ADDED", EventExecutionLimit, this.EventErrorHandler);
this._messageReactionRemoved = new AsyncEvent("MESSAGE_REACTION_REMOVED", EventExecutionLimit, this.EventErrorHandler);
this._messageReactionsCleared = new AsyncEvent("MESSAGE_REACTIONS_CLEARED", EventExecutionLimit, this.EventErrorHandler);
this._messageReactionRemovedEmoji = new AsyncEvent("MESSAGE_REACTION_REMOVED_EMOJI", EventExecutionLimit, this.EventErrorHandler);
this._webhooksUpdated = new AsyncEvent("WEBHOOKS_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._heartbeated = new AsyncEvent("HEARTBEATED", EventExecutionLimit, this.EventErrorHandler);
this._applicationCommandCreated = new AsyncEvent("APPLICATION_COMMAND_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._applicationCommandUpdated = new AsyncEvent("APPLICATION_COMMAND_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._applicationCommandDeleted = new AsyncEvent("APPLICATION_COMMAND_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._guildApplicationCommandCountUpdated = new AsyncEvent("GUILD_APPLICATION_COMMAND_COUNTS_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._applicationCommandPermissionsUpdated = new AsyncEvent("APPLICATION_COMMAND_PERMISSIONS_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildIntegrationCreated = new AsyncEvent("INTEGRATION_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._guildIntegrationUpdated = new AsyncEvent("INTEGRATION_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildIntegrationDeleted = new AsyncEvent("INTEGRATION_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._stageInstanceCreated = new AsyncEvent("STAGE_INSTANCE_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._stageInstanceUpdated = new AsyncEvent("STAGE_INSTANCE_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._stageInstanceDeleted = new AsyncEvent("STAGE_INSTANCE_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._threadCreated = new AsyncEvent("THREAD_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._threadUpdated = new AsyncEvent("THREAD_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._threadDeleted = new AsyncEvent("THREAD_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._threadListSynced = new AsyncEvent("THREAD_LIST_SYNCED", EventExecutionLimit, this.EventErrorHandler);
this._threadMemberUpdated = new AsyncEvent("THREAD_MEMBER_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._threadMembersUpdated = new AsyncEvent("THREAD_MEMBERS_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._zombied = new AsyncEvent("ZOMBIED", EventExecutionLimit, this.EventErrorHandler);
this._payloadReceived = new AsyncEvent("PAYLOAD_RECEIVED", EventExecutionLimit, this.EventErrorHandler);
this._guildScheduledEventCreated = new AsyncEvent("GUILD_SCHEDULED_EVENT_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._guildScheduledEventUpdated = new AsyncEvent("GUILD_SCHEDULED_EVENT_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildScheduledEventDeleted = new AsyncEvent("GUILD_SCHEDULED_EVENT_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._guildScheduledEventUserAdded = new AsyncEvent("GUILD_SCHEDULED_EVENT_USER_ADDED", EventExecutionLimit, this.EventErrorHandler);
this._guildScheduledEventUserRemoved = new AsyncEvent("GUILD_SCHEDULED_EVENT_USER_REMOVED", EventExecutionLimit, this.EventErrorHandler);
this._embeddedActivityUpdated = new AsyncEvent("EMBEDDED_ACTIVITY_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildMemberTimeoutAdded = new AsyncEvent("GUILD_MEMBER_TIMEOUT_ADDED", EventExecutionLimit, this.EventErrorHandler);
this._guildMemberTimeoutChanged = new AsyncEvent("GUILD_MEMBER_TIMEOUT_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildMemberTimeoutRemoved = new AsyncEvent("GUILD_MEMBER_TIMEOUT_REMOVED", EventExecutionLimit, this.EventErrorHandler);
this._rateLimitHit = new AsyncEvent("RATELIMIT_HIT", EventExecutionLimit, this.EventErrorHandler);
this._automodRuleCreated = new AsyncEvent("AUTO_MODERATION_RULE_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._automodRuleUpdated = new AsyncEvent("AUTO_MODERATION_RULE_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._automodRuleDeleted = new AsyncEvent("AUTO_MODERATION_RULE_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._automodActionExecuted = new AsyncEvent("AUTO_MODERATION_ACTION_EXECUTED", EventExecutionLimit, this.EventErrorHandler);
this._guildAuditLogEntryCreated = new AsyncEvent("GUILD_AUDIT_LOG_ENTRY_CREATED", EventExecutionLimit, this.EventErrorHandler);
this.GuildsInternal.Clear();
this._presencesLazy = new Lazy>(() => new ReadOnlyDictionary(this.PresencesInternal));
this._embeddedActivitiesLazy = new Lazy>(() => new ReadOnlyDictionary(this.EmbeddedActivitiesInternal));
}
#endregion
#region Client Extension Methods
///
/// Registers an extension with this client.
///
/// Extension to register.
public void AddExtension(BaseExtension ext)
{
ext.Setup(this);
this._extensions.Add(ext);
}
///
/// Retrieves a previously registered extension from this client.
///
/// The type of extension to retrieve.
/// The requested extension.
public T GetExtension() where T : BaseExtension
=> this._extensions.FirstOrDefault(x => x.GetType() == typeof(T)) as T;
#endregion
#region Public Connection Methods
///
/// Connects to the gateway.
///
/// The activity to set. Defaults to null.
/// The optional status to set. Defaults to null.
/// Since when is the client performing the specified activity. Defaults to null.
/// Thrown when an invalid token was provided.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task ConnectAsync(DiscordActivity activity = null, UserStatus? status = null, DateTimeOffset? idlesince = null)
{
// Check if connection lock is already set, and set it if it isn't
if (!this._connectionLock.Wait(0))
throw new InvalidOperationException("This client is already connected.");
this._connectionLock.Set();
var w = 7500;
var i = 5;
var s = false;
Exception cex = null;
if (activity == null && status == null && idlesince == null)
this._status = null;
else
{
var sinceUnix = idlesince != null ? (long?)Utilities.GetUnixTime(idlesince.Value) : null;
this._status = new StatusUpdate()
{
Activity = new TransportActivity(activity),
Status = status ?? UserStatus.Online,
IdleSince = sinceUnix,
IsAfk = idlesince != null,
ActivityInternal = activity
};
}
if (!this.IsShard)
{
if (this.Configuration.TokenType != TokenType.Bot)
this.Logger.LogWarning(LoggerEvents.Misc, "You are logging in with a token that is not a bot token. This is not officially supported by Discord, and can result in your account being terminated if you aren't careful.");
this.Logger.LogInformation(LoggerEvents.Startup, "Lib {0}, version {1}", this.BotLibrary, this.VersionString);
}
while (i-- > 0 || this.Configuration.ReconnectIndefinitely)
{
try
{
await this.InternalConnectAsync().ConfigureAwait(false);
s = true;
break;
}
catch (UnauthorizedException e)
{
FailConnection(this._connectionLock);
throw new Exception("Authentication failed. Check your token and try again.", e);
}
catch (PlatformNotSupportedException)
{
FailConnection(this._connectionLock);
throw;
}
catch (NotImplementedException)
{
FailConnection(this._connectionLock);
throw;
}
catch (Exception ex)
{
FailConnection(null);
cex = ex;
if (i <= 0 && !this.Configuration.ReconnectIndefinitely) break;
this.Logger.LogError(LoggerEvents.ConnectionFailure, ex, "Connection attempt failed, retrying in {0}s", w / 1000);
await Task.Delay(w).ConfigureAwait(false);
if (i > 0)
w *= 2;
}
}
if (!s && cex != null)
{
this._connectionLock.Set();
throw new Exception("Could not connect to Discord.", cex);
}
// non-closure, hence args
static void FailConnection(ManualResetEventSlim cl) =>
// unlock this (if applicable) so we can let others attempt to connect
cl?.Set();
}
///
/// Reconnects to the gateway.
///
/// Whether to start a new session.
public Task ReconnectAsync(bool startNewSession = true)
=> this.InternalReconnectAsync(startNewSession, code: startNewSession ? 1000 : 4002);
///
/// Disconnects from the gateway.
///
public async Task DisconnectAsync()
{
this.Configuration.AutoReconnect = false;
if (this.WebSocketClient != null)
await this.WebSocketClient.DisconnectAsync().ConfigureAwait(false);
}
#endregion
#region Public REST Methods
///
/// Gets a user.
///
/// Id of the user
/// Whether to ignore the cache. Defaults to false.
/// The requested user.
/// Thrown when the user does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task GetUserAsync(ulong userId, bool fetch = false)
{
if (!fetch)
{
return this.TryGetCachedUserInternal(userId, out var usr) ? usr : new DiscordUser { Id = userId, Discord = this };
}
else
{
var usr = await this.ApiClient.GetUserAsync(userId).ConfigureAwait(false);
usr = this.UserCache.AddOrUpdate(userId, usr, (id, old) =>
{
old.Username = usr.Username;
old.Discriminator = usr.Discriminator;
old.AvatarHash = usr.AvatarHash;
old.BannerHash = usr.BannerHash;
old.BannerColorInternal = usr.BannerColorInternal;
old.AvatarDecorationHash = usr.AvatarDecorationHash;
old.ThemeColorsInternal = usr.ThemeColorsInternal;
old.Pronouns = usr.Pronouns;
old.DisplayName = usr.DisplayName;
return old;
});
return usr;
}
}
///
/// Gets a applications rpc information.
///
/// Id of the application
/// The requested application.
/// Thrown when the application does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task GetRpcApplicationAsync(ulong applicationId)
=> await this.ApiClient.GetApplicationRpcInfoAsync(applicationId);
public async Task GetCurrentApplicationInfoAsync()
{
var tapp = await this.ApiClient.GetCurrentApplicationInfoAsync();
var app = new DiscordApplication
{
Discord = this,
Id = tapp.Id,
Name = tapp.Name,
Description = tapp.Description,
Summary = tapp.Summary,
IconHash = tapp.IconHash,
RpcOrigins = tapp.RpcOrigins != null ? new ReadOnlyCollection(tapp.RpcOrigins) : null,
Flags = tapp.Flags,
IsHook = tapp.IsHook,
Type = tapp.Type,
PrivacyPolicyUrl = tapp.PrivacyPolicyUrl,
TermsOfServiceUrl = tapp.TermsOfServiceUrl,
CustomInstallUrl = tapp.CustomInstallUrl,
InstallParams = tapp.InstallParams,
RoleConnectionsVerificationUrl = tapp.RoleConnectionsVerificationUrl,
- Tags = (tapp.Tags ?? Enumerable.Empty()).ToArray()
+ Tags = (tapp.Tags ?? Enumerable.Empty()).ToArray(),
+ GuildId = tapp.GuildId.ValueOrDefault(),
+ Slug = tapp.Slug.ValueOrDefault(),
+ PrimarySkuId = tapp.PrimarySkuId.ValueOrDefault(),
+ VerifyKey = tapp.VerifyKey.ValueOrDefault(),
+ CoverImageHash = tapp.CoverImageHash.ValueOrDefault(),
+ Guild = tapp.Guild.ValueOrDefault(),
+ ApproximateGuildCount = tapp.ApproximateGuildCount.ValueOrDefault(),
+ RequiresCodeGrant = tapp.BotRequiresCodeGrant.ValueOrDefault(),
+ IsPublic = tapp.IsPublicBot.ValueOrDefault(),
+ RedirectUris = tapp.RedirectUris.ValueOrDefault(),
+ InteractionsEndpointUrl = tapp.InteractionsEndpointUrl.ValueOrDefault()
};
-
- app.GuildId = tapp.GuildId.ValueOrDefault();
- app.Slug = tapp.Slug.ValueOrDefault();
- app.PrimarySkuId = tapp.PrimarySkuId.ValueOrDefault();
- app.VerifyKey = tapp.VerifyKey.ValueOrDefault();
- app.CoverImageHash = tapp.CoverImageHash.ValueOrDefault();
- app.Guild = tapp.Guild.ValueOrDefault();
- app.ApproximateGuildCount = tapp.ApproximateGuildCount.ValueOrDefault();
- app.RequiresCodeGrant = tapp.BotRequiresCodeGrant.ValueOrDefault();
- app.IsPublic = tapp.IsPublicBot.ValueOrDefault();
- app.RedirectUris = tapp.RedirectUris.ValueOrDefault();
- app.InteractionsEndpointUrl = tapp.InteractionsEndpointUrl.ValueOrDefault();
-
return app;
}
///
/// Tries to get a user.
///
/// Id of the user.
/// Whether to ignore the cache. Defaults to true.
/// The requested user or null if not found.
/// 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 TryGetUserAsync(ulong userId, 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.GetUserAsync(userId, fetch).ConfigureAwait(false);
}
catch (NotFoundException)
{
return null;
}
}
///
/// Tries to get the published store sku listings (premium application subscription).
///
/// The application id to fetch the listenings for.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
/// A list of published listings with s.
public async Task> TryGetPublishedListingsAsync(ulong applicationId)
{
try
{
return await this.ApiClient.GetPublishedListingsAsync(applicationId);
}
catch (NotFoundException)
{
return null;
}
}
///
/// Gets the applications role connection metadata.
///
/// A list of metadata records or .
public async Task> GetRoleConnectionMetadata()
=> await this.ApiClient.GetRoleConnectionMetadataRecords(this.CurrentApplication.Id);
///
/// Updates the applications role connection metadata.
///
/// A list of metadata objects. Max 5.
public async Task> UpdateRoleConnectionMetadata(IEnumerable metadata)
=> await this.ApiClient.UpdateRoleConnectionMetadataRecords(this.CurrentApplication.Id, metadata);
///
/// Removes all global application commands.
///
public async Task RemoveGlobalApplicationCommandsAsync()
=> await this.ApiClient.BulkOverwriteGlobalApplicationCommandsAsync(this.CurrentApplication.Id, Array.Empty());
///
/// Removes all global application commands for a specific guild id.
///
/// The target guild id.
public async Task RemoveGuildApplicationCommandsAsync(ulong guildId)
=> await this.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId, Array.Empty());
///
/// Removes all global application commands for a specific guild.
///
/// The target guild.
public async Task RemoveGuildApplicationCommandsAsync(DiscordGuild guild)
=> await this.RemoveGuildApplicationCommandsAsync(guild.Id);
///
/// Gets a channel.
///
/// The id of the channel to get.
/// Whether to ignore the cache. Defaults to false.
/// The requested channel.
/// 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 GetChannelAsync(ulong id, bool fetch = false)
=> (fetch ? null : this.InternalGetCachedChannel(id)) ?? await this.ApiClient.GetChannelAsync(id).ConfigureAwait(false);
///
/// Tries to get a channel.
///
/// The id of the channel to get.
/// Whether to ignore the cache. Defaults to true.
/// The requested channel or null if not found.
/// 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 TryGetChannelAsync(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.GetChannelAsync(id, fetch).ConfigureAwait(false);
}
catch (NotFoundException)
{
return null;
}
}
///
/// Gets a thread.
///
/// The id of the thread to get.
/// Whether to ignore the cache. Defaults to false.
/// The requested 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 async Task GetThreadAsync(ulong id, bool fetch = false)
=> (fetch ? null : this.InternalGetCachedThread(id)) ?? await this.ApiClient.GetThreadAsync(id).ConfigureAwait(false);
///
/// Tries to get a thread.
///
/// The id of the thread to get.
/// Whether to ignore the cache. Defaults to true.
/// The requested thread or null if not found.
/// 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 TryGetThreadAsync(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.GetThreadAsync(id, fetch).ConfigureAwait(false);
}
catch (NotFoundException)
{
return null;
}
}
///
/// Sends a normal message.
///
/// The channel to send to.
/// The message content to send.
/// The message that was sent.
/// 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 SendMessageAsync(DiscordChannel channel, string content)
=> this.ApiClient.CreateMessageAsync(channel.Id, content, embeds: null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false);
///
/// Sends a message with an embed.
///
/// The channel to send to.
/// The embed to attach to the message.
/// The message that was sent.
/// 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 SendMessageAsync(DiscordChannel channel, DiscordEmbed embed)
=> this.ApiClient.CreateMessageAsync(channel.Id, null, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false);
///
/// Sends a message with content and an embed.
///
/// Channel to send to.
/// The message content to send.
/// The embed to attach to the message.
/// The message that was sent.
/// 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 SendMessageAsync(DiscordChannel channel, string content, DiscordEmbed embed)
=> this.ApiClient.CreateMessageAsync(channel.Id, content, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false);
///
/// Sends a message with the .
///
/// The channel to send the message to.
/// The message builder.
/// The message that was sent.
/// Thrown when the client does not have the permission if TTS is false 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(DiscordChannel channel, DiscordMessageBuilder builder)
=> this.ApiClient.CreateMessageAsync(channel.Id, builder);
///
/// Sends a message with an .
///
/// The channel to send the message to.
/// The message builder.
/// The message that was sent.
/// Thrown when the client does not have the permission if TTS is false 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(DiscordChannel channel, Action action)
{
var builder = new DiscordMessageBuilder();
action(builder);
return this.ApiClient.CreateMessageAsync(channel.Id, builder);
}
///
/// Creates a guild. This requires the bot to be in less than 10 guilds total.
///
/// Name of the guild.
/// Voice region of the guild.
/// Stream containing the icon for the guild.
/// Verification level for the guild.
/// Default message notification settings for the guild.
/// System channel flags for the guild.
/// The created guild.
/// 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 CreateGuildAsync(string name, string region = null, Optional icon = default, VerificationLevel? verificationLevel = null,
DefaultMessageNotifications? defaultMessageNotifications = null, SystemChannelFlags? systemChannelFlags = null)
{
var iconb64 = ImageTool.Base64FromStream(icon);
return this.ApiClient.CreateGuildAsync(name, region, iconb64, verificationLevel, defaultMessageNotifications, systemChannelFlags);
}
///
/// Creates a guild from a template. This requires the bot to be in less than 10 guilds total.
///
/// The template code.
/// Name of the guild.
/// Stream containing the icon for the guild.
/// The created guild.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task CreateGuildFromTemplateAsync(string code, string name, Optional icon = default)
{
var iconb64 = ImageTool.Base64FromStream(icon);
return this.ApiClient.CreateGuildFromTemplateAsync(code, name, iconb64);
}
///
/// Gets a guild.
/// Setting to true will make a REST request.
///
/// The guild ID to search for.
/// Whether to include approximate presence and member counts in the returned guild.
/// Whether to ignore the cache. Defaults to false.
/// The requested Guild.
/// Thrown when the guild does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task GetGuildAsync(ulong id, bool? withCounts = null, bool fetch = false)
{
if (!fetch && this.GuildsInternal.TryGetValue(id, out var guild) && (!withCounts.HasValue || !withCounts.Value))
return guild;
guild = await this.ApiClient.GetGuildAsync(id, withCounts).ConfigureAwait(false);
var channels = await this.ApiClient.GetGuildChannelsAsync(guild.Id).ConfigureAwait(false);
foreach (var channel in channels) guild.ChannelsInternal[channel.Id] = channel;
return guild;
}
///
/// Tries to get a guild.
/// Setting to true will make a REST request.
///
/// The guild ID to search for.
/// Whether to include approximate presence and member counts in the returned guild.
/// Whether to ignore the cache. Defaults to true.
/// The requested Guild or null if not found.
/// 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 TryGetGuildAsync(ulong id, bool? withCounts = null, 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.GetGuildAsync(id, withCounts, fetch).ConfigureAwait(false);
}
catch (NotFoundException)
{
return null;
}
}
///
/// Gets a guild preview.
///
/// The guild ID.
/// A preview of the requested guild.
/// Thrown when the guild does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task GetGuildPreviewAsync(ulong id)
=> this.ApiClient.GetGuildPreviewAsync(id);
///
/// Tries to get a guild preview.
///
/// The guild ID.
/// A preview of the requested guild or null if not found.
/// 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 TryGetGuildPreviewAsync(ulong id)
#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.ApiClient.GetGuildPreviewAsync(id).ConfigureAwait(false);
}
catch (NotFoundException)
{
return null;
}
}
///
/// Gets a guild widget.
///
/// The Guild Id.
/// A guild widget.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task GetGuildWidgetAsync(ulong id)
=> this.ApiClient.GetGuildWidgetAsync(id);
///
/// Tries to get a guild widget.
///
/// The Guild Id.
/// The requested guild widget or null if not found.
/// 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 TryGetGuildWidgetAsync(ulong id)
#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.ApiClient.GetGuildWidgetAsync(id);
}
catch (NotFoundException)
{
return null;
}
}
///
/// Gets an invite.
///
/// The invite code.
/// Whether to include presence and total member counts in the returned invite.
/// Whether to include the expiration date in the returned invite.
/// The scheduled event id.
/// The requested invite.
/// Thrown when the invite does not exists.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task GetInviteByCodeAsync(string code, bool? withCounts = null, bool? withExpiration = null, ulong? scheduledEventId = null)
=> this.ApiClient.GetInviteAsync(code, withCounts, withExpiration, scheduledEventId);
///
/// Tries to get an invite.
///
/// The invite code.
/// Whether to include presence and total member counts in the returned invite.
/// Whether to include the expiration date in the returned invite.
/// The scheduled event id.
/// The requested invite or null if not found.
/// 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 TryGetInviteByCodeAsync(string code, bool? withCounts = null, bool? withExpiration = null, ulong? scheduledEventId = null)
#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.GetInviteByCodeAsync(code, withCounts, withExpiration, scheduledEventId).ConfigureAwait(false);
}
catch (NotFoundException)
{
return null;
}
}
///
/// Gets a list of user connections.
///
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task> GetConnectionsAsync()
=> this.ApiClient.GetUserConnectionsAsync();
///
/// Gets a sticker.
///
/// The requested sticker.
/// The id of the sticker.
/// Thrown when the sticker does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task GetStickerAsync(ulong id)
=> this.ApiClient.GetStickerAsync(id);
///
/// Tries to get a sticker.
///
/// The requested sticker or null if not found.
/// The id of the sticker.
/// 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 TryGetStickerAsync(ulong id)
#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.GetStickerAsync(id).ConfigureAwait(false);
}
catch (NotFoundException)
{
return null;
}
}
///
/// Gets all nitro sticker packs.
///
/// List of sticker packs.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task> GetStickerPacksAsync()
=> this.ApiClient.GetStickerPacksAsync();
///
/// Gets the In-App OAuth Url.
///
/// Defaults to .
/// Redirect Uri.
/// Defaults to .
/// The OAuth Url
public Uri GetInAppOAuth(Permissions permissions = Permissions.None, OAuthScopes scopes = OAuthScopes.BOT_DEFAULT, string redir = null)
{
permissions &= PermissionMethods.FullPerms;
return new Uri(new QueryUriBuilder($"{DiscordDomain.GetDomain(CoreDomain.Discord).Url}{Endpoints.OAUTH2}{Endpoints.AUTHORIZE}")
.AddParameter("client_id", this.CurrentApplication.Id.ToString(CultureInfo.InvariantCulture))
.AddParameter("scope", OAuth.ResolveScopes(scopes))
.AddParameter("permissions", ((long)permissions).ToString(CultureInfo.InvariantCulture))
.AddParameter("state", "")
.AddParameter("redirect_uri", redir ?? "")
.ToString());
}
///
/// Generates an In-App OAuth Url.
///
/// The bot to generate the url for.
/// Defaults to .
/// Redirect Uri.
/// Defaults to .
/// The OAuth Url
public Uri GenerateInAppOauthFor(DiscordUser bot, Permissions permissions = Permissions.None, OAuthScopes scopes = OAuthScopes.BOT_DEFAULT, string redir = null)
{
if (!bot.IsBot)
throw new ArgumentException("The user must be a bot.", nameof(bot));
permissions &= PermissionMethods.FullPerms;
return new Uri(new QueryUriBuilder($"{DiscordDomain.GetDomain(CoreDomain.Discord).Url}{Endpoints.OAUTH2}{Endpoints.AUTHORIZE}")
.AddParameter("client_id", bot.Id.ToString(CultureInfo.InvariantCulture))
.AddParameter("scope", OAuth.ResolveScopes(scopes))
.AddParameter("permissions", ((long)permissions).ToString(CultureInfo.InvariantCulture))
.AddParameter("state", "")
.AddParameter("redirect_uri", redir ?? "")
.ToString());
}
///
/// Gets a webhook.
///
/// The target webhook id.
/// The requested webhook.
/// Thrown when the webhook does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task GetWebhookAsync(ulong id)
=> this.ApiClient.GetWebhookAsync(id);
///
/// Tries to get a webhook.
///
/// The target webhook id.
/// The requested webhook or null if not found.
/// 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 TryGetWebhookAsync(ulong id)
#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.GetWebhookAsync(id).ConfigureAwait(false);
}
catch (NotFoundException)
{
return null;
}
}
///
/// Gets a webhook with a token.
///
/// The target webhook id.
/// The target webhook token.
/// The requested webhook.
/// Thrown when the webhook does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task GetWebhookWithTokenAsync(ulong id, string token)
=> this.ApiClient.GetWebhookWithTokenAsync(id, token);
///
/// Tries to get a webhook with a token.
///
/// The target webhook id.
/// The target webhook token.
/// The requested webhook or null if not found.
/// 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 TryGetWebhookWithTokenAsync(ulong id, string token)
#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.GetWebhookWithTokenAsync(id, token).ConfigureAwait(false);
}
catch (NotFoundException)
{
return null;
}
}
///
/// Updates current user's activity and status.
///
/// Activity to set.
/// Status of the user.
/// Since when is the client performing the specified activity.
///
public Task UpdateStatusAsync(DiscordActivity activity = null, UserStatus? userStatus = null, DateTimeOffset? idleSince = null)
=> this.InternalUpdateStatusAsync(activity, userStatus, idleSince);
///
/// Edits current user.
///
/// New username.
/// New avatar.
/// The modified user.
/// Thrown when the user does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task UpdateCurrentUserAsync(string username = null, Optional avatar = default)
{
var av64 = ImageTool.Base64FromStream(avatar);
var usr = await this.ApiClient.ModifyCurrentUserAsync(username, av64).ConfigureAwait(false);
this.CurrentUser.Username = usr.Username;
this.CurrentUser.Discriminator = usr.Discriminator;
this.CurrentUser.AvatarHash = usr.AvatarHash;
return this.CurrentUser;
}
///
/// Gets a guild template by the code.
///
/// The code of the template.
/// The guild template for the code.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task GetTemplateAsync(string code)
=> this.ApiClient.GetTemplateAsync(code);
///
/// Gets all the global application commands for this application.
///
/// Whether to get the full localization dict.
/// A list of global application commands.
public Task> GetGlobalApplicationCommandsAsync(bool withLocalizations = false) =>
this.ApiClient.GetGlobalApplicationCommandsAsync(this.CurrentApplication.Id, withLocalizations);
///
/// Overwrites the existing global application commands. New commands are automatically created and missing commands are automatically deleted.
///
/// The list of commands to overwrite with.
/// The list of global commands.
public Task> BulkOverwriteGlobalApplicationCommandsAsync(IEnumerable commands) =>
this.ApiClient.BulkOverwriteGlobalApplicationCommandsAsync(this.CurrentApplication.Id, commands);
///
/// Creates or overwrites a global application command.
///
/// The command to create.
/// The created command.
public Task CreateGlobalApplicationCommandAsync(DiscordApplicationCommand command) =>
this.ApiClient.CreateGlobalApplicationCommandAsync(this.CurrentApplication.Id, command);
///
/// Gets a global application command by its id.
///
/// The id of the command to get.
/// The command with the id.
public Task GetGlobalApplicationCommandAsync(ulong commandId) =>
this.ApiClient.GetGlobalApplicationCommandAsync(this.CurrentApplication.Id, commandId);
///
/// Edits a global application command.
///
/// The id of the command to edit.
/// Action to perform.
/// The edited command.
public async Task EditGlobalApplicationCommandAsync(ulong commandId, Action action)
{
var mdl = new ApplicationCommandEditModel();
action(mdl);
var applicationId = this.CurrentApplication?.Id ?? (await this.GetCurrentApplicationAsync().ConfigureAwait(false)).Id;
return await this.ApiClient.EditGlobalApplicationCommandAsync(applicationId, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.NameLocalizations, mdl.DescriptionLocalizations, mdl.DefaultMemberPermissions, mdl.DmPermission, mdl.IsNsfw).ConfigureAwait(false);
}
///
/// Deletes a global application command.
///
/// The id of the command to delete.
public Task DeleteGlobalApplicationCommandAsync(ulong commandId) =>
this.ApiClient.DeleteGlobalApplicationCommandAsync(this.CurrentApplication.Id, commandId);
///
/// Gets all the application commands for a guild.
///
/// The id of the guild to get application commands for.
/// Whether to get the full localization dict.
/// A list of application commands in the guild.
public Task> GetGuildApplicationCommandsAsync(ulong guildId, bool withLocalizations = false) =>
this.ApiClient.GetGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId, withLocalizations);
///
/// Overwrites the existing application commands in a guild. New commands are automatically created and missing commands are automatically deleted.
///
/// The id of the guild.
/// The list of commands to overwrite with.
/// The list of guild commands.
public Task> BulkOverwriteGuildApplicationCommandsAsync(ulong guildId, IEnumerable commands) =>
this.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId, commands);
///
/// Creates or overwrites a guild application command.
///
/// The id of the guild to create the application command in.
/// The command to create.
/// The created command.
public Task CreateGuildApplicationCommandAsync(ulong guildId, DiscordApplicationCommand command) =>
this.ApiClient.CreateGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, command);
///
/// Gets a application command in a guild by its id.
///
/// The id of the guild the application command is in.
/// The id of the command to get.
/// The command with the id.
public Task GetGuildApplicationCommandAsync(ulong guildId, ulong commandId) =>
this.ApiClient.GetGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId);
///
/// Edits a application command in a guild.
///
/// The id of the guild the application command is in.
/// The id of the command to edit.
/// Action to perform.
/// The edited command.
public async Task EditGuildApplicationCommandAsync(ulong guildId, ulong commandId, Action action)
{
var mdl = new ApplicationCommandEditModel();
action(mdl);
var applicationId = this.CurrentApplication?.Id ?? (await this.GetCurrentApplicationAsync().ConfigureAwait(false)).Id;
return await this.ApiClient.EditGuildApplicationCommandAsync(applicationId, guildId, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.NameLocalizations, mdl.DescriptionLocalizations, mdl.DefaultMemberPermissions, mdl.DmPermission, mdl.IsNsfw).ConfigureAwait(false);
}
///
/// Deletes a application command in a guild.
///
/// The id of the guild to delete the application command in.
/// The id of the command.
public Task DeleteGuildApplicationCommandAsync(ulong guildId, ulong commandId) =>
this.ApiClient.DeleteGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId);
///
/// Gets all command permissions for a guild.
///
/// The target guild.
public Task> GetGuildApplicationCommandPermissionsAsync(ulong guildId) =>
this.ApiClient.GetGuildApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId);
///
/// Gets the permissions for a guild command.
///
/// The target guild.
/// The target command id.
public Task GetApplicationCommandPermissionAsync(ulong guildId, ulong commandId) =>
this.ApiClient.GetGuildApplicationCommandPermissionAsync(this.CurrentApplication.Id, guildId, commandId);
#endregion
#region Internal Caching Methods
///
/// Gets the internal cached threads.
///
/// The target thread id.
/// The requested thread.
internal DiscordThreadChannel InternalGetCachedThread(ulong threadId)
{
if (this.Guilds == null)
return null;
foreach (var guild in this.Guilds.Values)
if (guild.Threads.TryGetValue(threadId, out var foundThread))
return foundThread;
return null;
}
///
/// Gets the internal cached scheduled event.
///
/// The target scheduled event id.
/// The requested scheduled event.
internal DiscordScheduledEvent InternalGetCachedScheduledEvent(ulong scheduledEventId)
{
if (this.Guilds == null)
return null;
foreach (var guild in this.Guilds.Values)
if (guild.ScheduledEvents.TryGetValue(scheduledEventId, out var foundScheduledEvent))
return foundScheduledEvent;
return null;
}
///
/// Gets the internal cached channel.
///
/// The target channel id.
/// The requested channel.
internal DiscordChannel InternalGetCachedChannel(ulong channelId, ulong? guildId = null)
{
if (this.Guilds == null)
return null;
foreach (var guild in this.Guilds.Values)
if (guild.Channels.TryGetValue(channelId, out var foundChannel))
{
if (guildId.HasValue)
foundChannel.GuildId = guildId;
return foundChannel;
}
return null;
}
///
/// Gets the internal cached guild.
///
/// The target guild id.
/// The requested guild.
internal DiscordGuild InternalGetCachedGuild(ulong? guildId)
{
if (this.GuildsInternal != null && guildId.HasValue)
{
if (this.GuildsInternal.TryGetValue(guildId.Value, out var guild))
return guild;
}
return null;
}
///
/// Updates a message.
///
/// The message to update.
/// The author to update.
/// The guild to update.
/// The member to update.
private void UpdateMessage(DiscordMessage message, TransportUser author, DiscordGuild guild, TransportMember member)
{
if (author != null)
{
var usr = new DiscordUser(author) { Discord = this };
if (member != null)
member.User = author;
message.Author = this.UpdateUser(usr, guild?.Id, guild, member);
}
var channel = this.InternalGetCachedChannel(message.ChannelId);
if (channel != null) return;
channel = !message.GuildId.HasValue
? new DiscordDmChannel
{
Id = message.ChannelId,
Discord = this,
Type = ChannelType.Private
}
: new DiscordChannel
{
Id = message.ChannelId,
Discord = this
};
message.Channel = channel;
}
///
/// Updates a scheduled event.
///
/// The scheduled event to update.
/// The guild to update.
/// The updated scheduled event.
private DiscordScheduledEvent UpdateScheduledEvent(DiscordScheduledEvent scheduledEvent, DiscordGuild guild)
{
if (scheduledEvent != null)
{
_ = guild.ScheduledEventsInternal.AddOrUpdate(scheduledEvent.Id, scheduledEvent, (id, old) =>
{
old.Discord = this;
old.Description = scheduledEvent.Description;
old.ChannelId = scheduledEvent.ChannelId;
old.EntityId = scheduledEvent.EntityId;
old.EntityType = scheduledEvent.EntityType;
old.EntityMetadata = scheduledEvent.EntityMetadata;
old.PrivacyLevel = scheduledEvent.PrivacyLevel;
old.Name = scheduledEvent.Name;
old.Status = scheduledEvent.Status;
old.UserCount = scheduledEvent.UserCount;
old.ScheduledStartTimeRaw = scheduledEvent.ScheduledStartTimeRaw;
old.ScheduledEndTimeRaw = scheduledEvent.ScheduledEndTimeRaw;
return old;
});
}
return scheduledEvent;
}
///
/// Updates a user.
///
/// The user to update.
/// The guild id to update.
/// The guild to update.
/// The member to update.
/// The updated user.
private DiscordUser UpdateUser(DiscordUser usr, ulong? guildId, DiscordGuild guild, TransportMember mbr)
{
if (mbr != null)
{
if (mbr.User != null)
{
usr = new DiscordUser(mbr.User) { Discord = this };
_ = this.UserCache.AddOrUpdate(usr.Id, usr, (id, old) =>
{
old.Username = usr.Username;
old.Discriminator = usr.Discriminator;
old.AvatarHash = usr.AvatarHash;
old.BannerHash = usr.BannerHash;
old.BannerColorInternal = usr.BannerColorInternal;
old.AvatarDecorationHash = usr.AvatarDecorationHash;
old.ThemeColorsInternal = usr.ThemeColorsInternal;
old.Pronouns = usr.Pronouns;
old.Locale = usr.Locale;
old.DisplayName = usr.DisplayName;
return old;
});
usr = new DiscordMember(mbr) { Discord = this, GuildId = guildId.Value };
}
var intents = this.Configuration.Intents;
DiscordMember member = default;
if (!intents.HasAllPrivilegedIntents() || guild.IsLarge) // we have the necessary privileged intents, no need to worry about caching here unless guild is large.
{
if (guild?.MembersInternal.TryGetValue(usr.Id, out member) == false)
{
if (intents.HasIntent(DiscordIntents.GuildMembers) || this.Configuration.AlwaysCacheMembers) // member can be updated by events, so cache it
{
guild.MembersInternal.TryAdd(usr.Id, (DiscordMember)usr);
}
}
else if (intents.HasIntent(DiscordIntents.GuildPresences) || this.Configuration.AlwaysCacheMembers) // we can attempt to update it if it's already in cache.
{
if (!intents.HasIntent(DiscordIntents.GuildMembers)) // no need to update if we already have the member events
{
_ = guild.MembersInternal.TryUpdate(usr.Id, (DiscordMember)usr, member);
}
}
}
}
else if (usr.Username != null) // check if not a skeleton user
{
_ = this.UserCache.AddOrUpdate(usr.Id, usr, (id, old) =>
{
old.Username = usr.Username;
old.Discriminator = usr.Discriminator;
old.AvatarHash = usr.AvatarHash;
old.BannerHash = usr.BannerHash;
old.BannerColorInternal = usr.BannerColorInternal;
old.AvatarDecorationHash = usr.AvatarDecorationHash;
old.ThemeColorsInternal = usr.ThemeColorsInternal;
old.Pronouns = usr.Pronouns;
old.Locale = usr.Locale;
old.DisplayName = usr.DisplayName;
return old;
});
}
return usr;
}
///
/// Updates the cached scheduled events in a guild.
///
/// The guild.
/// The raw events.
private void UpdateCachedScheduledEvent(DiscordGuild guild, JArray rawEvents)
{
if (this._disposed)
return;
if (rawEvents != null)
{
guild.ScheduledEventsInternal.Clear();
foreach (var xj in rawEvents)
{
var xtm = xj.ToDiscordObject();
xtm.Discord = this;
guild.ScheduledEventsInternal[xtm.Id] = xtm;
}
}
}
///
/// Updates the cached guild.
///
/// The new guild.
/// The raw members.
private void UpdateCachedGuild(DiscordGuild newGuild, JArray rawMembers)
{
if (this._disposed)
return;
if (!this.GuildsInternal.ContainsKey(newGuild.Id))
this.GuildsInternal[newGuild.Id] = newGuild;
var guild = this.GuildsInternal[newGuild.Id];
if (newGuild.ChannelsInternal != null && !newGuild.ChannelsInternal.IsEmpty)
{
foreach (var channel in newGuild.ChannelsInternal.Values)
{
if (guild.ChannelsInternal.TryGetValue(channel.Id, out _)) continue;
channel.Initialize(this);
guild.ChannelsInternal[channel.Id] = channel;
}
}
if (newGuild.ThreadsInternal != null && !newGuild.ThreadsInternal.IsEmpty)
{
foreach (var thread in newGuild.ThreadsInternal.Values)
{
if (guild.ThreadsInternal.TryGetValue(thread.Id, out _)) continue;
guild.ThreadsInternal[thread.Id] = thread;
}
}
if (newGuild.ScheduledEventsInternal != null && !newGuild.ScheduledEventsInternal.IsEmpty)
{
foreach (var @event in newGuild.ScheduledEventsInternal.Values)
{
if (guild.ScheduledEventsInternal.TryGetValue(@event.Id, out _)) continue;
guild.ScheduledEventsInternal[@event.Id] = @event;
}
}
foreach (var newEmoji in newGuild.EmojisInternal.Values)
_ = guild.EmojisInternal.GetOrAdd(newEmoji.Id, _ => newEmoji);
foreach (var newSticker in newGuild.StickersInternal.Values)
_ = guild.StickersInternal.GetOrAdd(newSticker.Id, _ => newSticker);
foreach (var newStageInstance in newGuild.StageInstancesInternal.Values)
_ = guild.StageInstancesInternal.GetOrAdd(newStageInstance.Id, _ => newStageInstance);
if (rawMembers != null)
{
guild.MembersInternal.Clear();
foreach (var xj in rawMembers)
{
var xtm = xj.ToDiscordObject();
var xu = new DiscordUser(xtm.User) { Discord = this };
_ = this.UserCache.AddOrUpdate(xtm.User.Id, xu, (id, old) =>
{
old.Username = xu.Username;
old.Discriminator = xu.Discriminator;
old.AvatarHash = xu.AvatarHash;
old.PremiumType = xu.PremiumType;
old.DisplayName = xu.DisplayName;
return old;
});
guild.MembersInternal[xtm.User.Id] = new DiscordMember(xtm) { Discord = this, GuildId = guild.Id };
}
}
foreach (var role in newGuild.RolesInternal.Values)
{
if (guild.RolesInternal.TryGetValue(role.Id, out _)) continue;
role.GuildId = guild.Id;
guild.RolesInternal[role.Id] = role;
}
guild.Name = newGuild.Name;
guild.AfkChannelId = newGuild.AfkChannelId;
guild.AfkTimeout = newGuild.AfkTimeout;
guild.DefaultMessageNotifications = newGuild.DefaultMessageNotifications;
guild.RawFeatures = newGuild.RawFeatures;
guild.IconHash = newGuild.IconHash;
guild.MfaLevel = newGuild.MfaLevel;
guild.OwnerId = newGuild.OwnerId;
guild.VoiceRegionId = newGuild.VoiceRegionId;
guild.SplashHash = newGuild.SplashHash;
guild.VerificationLevel = newGuild.VerificationLevel;
guild.WidgetEnabled = newGuild.WidgetEnabled;
guild.WidgetChannelId = newGuild.WidgetChannelId;
guild.ExplicitContentFilter = newGuild.ExplicitContentFilter;
guild.PremiumTier = newGuild.PremiumTier;
guild.PremiumSubscriptionCount = newGuild.PremiumSubscriptionCount;
guild.PremiumProgressBarEnabled = newGuild.PremiumProgressBarEnabled;
guild.BannerHash = newGuild.BannerHash;
guild.Description = newGuild.Description;
guild.VanityUrlCode = newGuild.VanityUrlCode;
guild.SystemChannelId = newGuild.SystemChannelId;
guild.SystemChannelFlags = newGuild.SystemChannelFlags;
guild.DiscoverySplashHash = newGuild.DiscoverySplashHash;
guild.MaxMembers = newGuild.MaxMembers;
guild.MaxPresences = newGuild.MaxPresences;
guild.ApproximateMemberCount = newGuild.ApproximateMemberCount;
guild.ApproximatePresenceCount = newGuild.ApproximatePresenceCount;
guild.MaxVideoChannelUsers = newGuild.MaxVideoChannelUsers;
guild.PreferredLocale = newGuild.PreferredLocale;
guild.RulesChannelId = newGuild.RulesChannelId;
guild.PublicUpdatesChannelId = newGuild.PublicUpdatesChannelId;
guild.ApplicationId = newGuild.ApplicationId;
// fields not sent for update:
// - guild.Channels
// - voice states
// - guild.JoinedAt = new_guild.JoinedAt;
// - guild.Large = new_guild.Large;
// - guild.MemberCount = Math.Max(new_guild.MemberCount, guild._members.Count);
// - guild.Unavailable = new_guild.Unavailable;
}
///
/// Populates the message reactions and cache.
///
/// The message.
/// The author.
/// The member.
private void PopulateMessageReactionsAndCache(DiscordMessage message, TransportUser author, TransportMember member)
{
var guild = message.Channel?.Guild ?? this.InternalGetCachedGuild(message.GuildId);
this.UpdateMessage(message, author, guild, member);
message.ReactionsInternal ??= new List();
foreach (var xr in message.ReactionsInternal)
xr.Emoji.Discord = this;
if (this.Configuration.MessageCacheSize > 0 && message.Channel != null)
this.MessageCache?.Add(message);
}
#endregion
#region Disposal
~DiscordClient()
{
this.Dispose();
}
///
/// Whether the client is disposed.
///
private bool _disposed;
///
/// Disposes the client.
///
public override void Dispose()
{
if (this._disposed)
return;
this._disposed = true;
GC.SuppressFinalize(this);
this.DisconnectAsync().ConfigureAwait(false).GetAwaiter().GetResult();
this.ApiClient.Rest.Dispose();
this.CurrentUser = null;
var extensions = this._extensions; // prevent _extensions being modified during dispose
this._extensions = null;
foreach (var extension in extensions)
if (extension is IDisposable disposable)
disposable.Dispose();
try
{
this._cancelTokenSource?.Cancel();
this._cancelTokenSource?.Dispose();
}
catch { }
this.GuildsInternal = null;
this._heartbeatTask = null;
}
#endregion
}
diff --git a/DisCatSharp/Entities/Channel/DiscordChannel.cs b/DisCatSharp/Entities/Channel/DiscordChannel.cs
index 9f2b7aa59..ead5133fc 100644
--- a/DisCatSharp/Entities/Channel/DiscordChannel.cs
+++ b/DisCatSharp/Entities/Channel/DiscordChannel.cs
@@ -1,1471 +1,1470 @@
// 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
///
/// 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.
/// Audit log reason.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task ModifyStageAsync(Optional topic, 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/DiscordGuild.AuditLog.cs b/DisCatSharp/Entities/Guild/DiscordGuild.AuditLog.cs
index 070de5829..98833ed6b 100644
--- a/DisCatSharp/Entities/Guild/DiscordGuild.AuditLog.cs
+++ b/DisCatSharp/Entities/Guild/DiscordGuild.AuditLog.cs
@@ -1,1364 +1,1364 @@
// 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.Linq;
using System.Threading.Tasks;
using DisCatSharp.Enums;
using DisCatSharp.Exceptions;
using DisCatSharp.Net;
using DisCatSharp.Net.Abstractions;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
namespace DisCatSharp.Entities;
public partial class DiscordGuild
{
// TODO: Rework audit logs!
///
/// Gets audit log entries for this guild.
///
/// Maximum number of entries to fetch.
/// Filter by member responsible.
/// Filter by action type.
/// A collection of requested audit log entries.
/// Thrown when the client does not have the permission.
/// Thrown when Discord is unable to process the request.
public async Task> GetAuditLogsAsync(int? limit = null, DiscordMember byMember = null, AuditLogActionType? actionType = null)
{
var alrs = new List();
int ac = 1, tc = 0, rmn = 100;
var last = 0ul;
while (ac > 0)
{
rmn = limit != null ? limit.Value - tc : 100;
rmn = Math.Min(100, rmn);
if (rmn <= 0) break;
var alr = await this.Discord.ApiClient.GetAuditLogsAsync(this.Id, rmn, null, last == 0 ? null : last, byMember?.Id, (int?)actionType).ConfigureAwait(false);
ac = alr.Entries.Count;
tc += ac;
if (ac > 0)
{
last = alr.Entries[alr.Entries.Count - 1].Id;
alrs.Add(alr);
}
}
var auditLogResult = await this.ProcessAuditLog(alrs);
return auditLogResult;
}
///
/// Proceesses audit log objects.
///
/// A list of raw audit log objects.
/// The processed audit log list as readonly.
internal async Task> ProcessAuditLog(List