diff --git a/DisCatSharp/Clients/DiscordClient.cs b/DisCatSharp/Clients/DiscordClient.cs
index 2cf5d5994..3121ce9da 100644
--- a/DisCatSharp/Clients/DiscordClient.cs
+++ b/DisCatSharp/Clients/DiscordClient.cs
@@ -1,1296 +1,1303 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.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.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 = false)
=> 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 fetch the user again. Defaults to true.
/// The requested user.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task GetUserAsync(ulong userId, bool fetch = true)
{
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.Pronouns = usr.Pronouns;
return old;
});
return usr;
}
}
///
/// Gets a channel.
///
/// The id of the channel to get.
/// 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)
=> this.InternalGetCachedChannel(id) ?? await this.ApiClient.GetChannelAsync(id).ConfigureAwait(false);
///
/// Gets a thread.
///
/// The id of the thread to get.
/// 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)
=> this.InternalGetCachedThread(id) ?? await this.ApiClient.GetThreadAsync(id).ConfigureAwait(false);
///
/// 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);
}
///
/// Executes a raw request.
///
///
///
/// var request = await Client.ExecuteRawRequestAsync(RestRequestMethod.GET, $"{Endpoints.CHANNELS}/243184972190742178964/{Endpoints.INVITES}");
/// List<DiscordInvite> invites = DiscordJson.ToDiscordObject<List<DiscordInvite>>(request.Response);
///
///
/// The method.
/// The route.
/// The route parameters.
/// The json body.
/// The additional headers.
/// The ratelimit wait override.
/// Thrown when the resource does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
/// A awaitable RestResponse
[Obsolete("This is no longer needed. Use DiscordClient.RestClient instead.", false)]
public async Task ExecuteRawRequestAsync(RestRequestMethod method, string route, object routeParams, string jsonBody = null, Dictionary additionalHeaders = null, double? ratelimitWaitOverride = null)
{
var bucket = this.ApiClient.Rest.GetBucket(method, route, routeParams, out var path);
var url = Utilities.GetApiUriFor(path, this.Configuration);
var res = await this.ApiClient.DoRequestAsync(this, bucket, url, method, route, additionalHeaders, DiscordJson.SerializeObject(jsonBody), ratelimitWaitOverride);
return res;
}
///
/// 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.
/// 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)
{
if (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;
}
///
/// Gets a guild preview.
///
/// The guild ID.
///
/// 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);
///
/// 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);
///
/// 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);
///
/// 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;
// hey look, it's not all annoying and blue :P
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());
}
///
/// 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);
///
/// Gets a webhook.
///
/// 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);
///
/// 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)
{
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)
{
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)
{
foreach (var guild in this.Guilds.Values)
if (guild.Channels.TryGetValue(channelId, out var foundChannel))
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.Pronouns = usr.Pronouns;
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.Pronouns = usr.Pronouns;
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;
foreach (var overwrite in channel.PermissionOverwritesInternal)
{
overwrite.Discord = this;
overwrite.ChannelId = channel.Id;
}
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;
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);
if (message.ReactionsInternal == null)
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/User/DiscordUser.cs b/DisCatSharp/Entities/User/DiscordUser.cs
index 15c1d34f0..512c73571 100644
--- a/DisCatSharp/Entities/User/DiscordUser.cs
+++ b/DisCatSharp/Entities/User/DiscordUser.cs
@@ -1,458 +1,465 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
using DisCatSharp.Enums;
using DisCatSharp.Exceptions;
using DisCatSharp.Net;
using DisCatSharp.Net.Abstractions;
using Newtonsoft.Json;
namespace DisCatSharp.Entities;
///
/// Represents a Discord user.
///
public class DiscordUser : SnowflakeObject, IEquatable
{
///
/// Initializes a new instance of the class.
///
internal DiscordUser()
{ }
///
/// Initializes a new instance of the class.
///
/// The transport user.
internal DiscordUser(TransportUser transport)
{
this.Id = transport.Id;
this.Username = transport.Username;
this.Discriminator = transport.Discriminator;
this.AvatarHash = transport.AvatarHash;
this.BannerHash = transport.BannerHash;
this.BannerColorInternal = transport.BannerColor;
this.IsBot = transport.IsBot;
this.MfaEnabled = transport.MfaEnabled;
this.Verified = transport.Verified;
this.Email = transport.Email;
this.PremiumType = transport.PremiumType;
this.Locale = transport.Locale;
this.Flags = transport.Flags;
this.OAuthFlags = transport.OAuthFlags;
this.Bio = transport.Bio;
+ this.Pronouns = transport.Pronouns;
}
///
/// Gets this user's username.
///
[JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)]
public virtual string Username { get; internal set; }
///
/// Gets this user's username with the discriminator.
/// Example: Discord#0000
///
[JsonIgnore]
public virtual string UsernameWithDiscriminator
=> $"{this.Username}#{this.Discriminator}";
///
/// Gets the user's 4-digit discriminator.
///
[JsonProperty("discriminator", NullValueHandling = NullValueHandling.Ignore)]
public virtual string Discriminator { get; internal set; }
///
/// Gets the discriminator integer.
///
[JsonIgnore]
internal int DiscriminatorInt
=> int.Parse(this.Discriminator, NumberStyles.Integer, CultureInfo.InvariantCulture);
///
/// Gets the user's banner color, if set. Mutually exclusive with .
///
public virtual DiscordColor? BannerColor
=> !this.BannerColorInternal.HasValue ? null : new DiscordColor(this.BannerColorInternal.Value);
///
/// Gets the user's banner color integer.
///
[JsonProperty("accent_color")]
internal int? BannerColorInternal;
///
/// Gets the user's banner url
///
[JsonIgnore]
public string BannerUrl
=> string.IsNullOrWhiteSpace(this.BannerHash) ? null : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.BANNERS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.BannerHash}.{(this.BannerHash.StartsWith("a_") ? "gif" : "png")}?size=4096";
///
/// Gets the user's profile banner hash. Mutually exclusive with .
///
[JsonProperty("banner", NullValueHandling = NullValueHandling.Ignore)]
public virtual string BannerHash { get; internal set; }
///
/// Gets the users bio.
/// This is not available to bots tho.
///
[JsonProperty("bio", NullValueHandling = NullValueHandling.Ignore)]
public virtual string Bio { get; internal set; }
///
/// Gets the user's avatar hash.
///
[JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)]
public virtual string AvatarHash { get; internal set; }
///
/// Returns a uri to this users profile.
///
[JsonIgnore]
public Uri ProfileUri => new($"{DiscordDomain.GetDomain(CoreDomain.Discord).Url}{Endpoints.USERS}/{this.Id}");
///
/// Returns a string representing the direct URL to this users profile.
///
/// The URL of this users profile.
[JsonIgnore]
public string ProfileUrl => this.ProfileUri.AbsoluteUri;
///
/// Gets the user's avatar URL.s
///
[JsonIgnore]
public string AvatarUrl
=> string.IsNullOrWhiteSpace(this.AvatarHash) ? this.DefaultAvatarUrl : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.AvatarHash}.{(this.AvatarHash.StartsWith("a_") ? "gif" : "png")}?size=1024";
///
/// Gets the URL of default avatar for this user.
///
[JsonIgnore]
public string DefaultAvatarUrl
=> $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.EMBED}{Endpoints.AVATARS}/{(this.DiscriminatorInt % 5).ToString(CultureInfo.InvariantCulture)}.png?size=1024";
///
/// Gets whether the user is a bot.
///
[JsonProperty("bot", NullValueHandling = NullValueHandling.Ignore)]
public virtual bool IsBot { get; internal set; }
///
/// Gets whether the user has multi-factor authentication enabled.
///
[JsonProperty("mfa_enabled", NullValueHandling = NullValueHandling.Ignore)]
public virtual bool? MfaEnabled { get; internal set; }
///
/// Gets whether the user is an official Discord system user.
///
[JsonProperty("system", NullValueHandling = NullValueHandling.Ignore)]
public bool? IsSystem { get; internal set; }
///
/// Gets whether the user is verified.
/// This is only present in OAuth.
///
[JsonProperty("verified", NullValueHandling = NullValueHandling.Ignore)]
public virtual bool? Verified { get; internal set; }
///
/// Gets the user's email address.
/// This is only present in OAuth.
///
[JsonProperty("email", NullValueHandling = NullValueHandling.Ignore)]
public virtual string Email { get; internal set; }
///
/// Gets the user's premium type.
///
[JsonProperty("premium_type", NullValueHandling = NullValueHandling.Ignore)]
public virtual PremiumType? PremiumType { get; internal set; }
///
/// Gets the user's chosen language
///
[JsonProperty("locale", NullValueHandling = NullValueHandling.Ignore)]
public virtual string Locale { get; internal set; }
///
/// Gets the user's flags for OAuth.
///
[JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)]
public virtual UserFlags? OAuthFlags { get; internal set; }
///
/// Gets the user's flags.
///
[JsonProperty("public_flags", NullValueHandling = NullValueHandling.Ignore)]
public virtual UserFlags? Flags { get; internal set; }
+ ///
+ /// Gets the user's pronouns.
+ ///
+ [JsonProperty("pronouns", NullValueHandling = NullValueHandling.Ignore)]
+ public virtual string Pronouns { get; internal set; }
+
///
/// Gets the user's mention string.
///
[JsonIgnore]
public string Mention
=> Formatter.Mention(this, this is DiscordMember);
///
/// Gets whether this user is the Client which created this object.
///
[JsonIgnore]
public bool IsCurrent
=> this.Id == this.Discord.CurrentUser.Id;
#region Extension of DiscordUser
///
/// Whether this member is a
///
///
[JsonIgnore]
public bool IsMod
=> this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.CertifiedModerator);
///
/// Whether this member is a
///
///
[JsonIgnore]
public bool IsPartner
=> this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.Partner);
///
/// Whether this member is a
///
///
[JsonIgnore]
public bool IsVerifiedBot
=> this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.VerifiedBot);
///
/// Whether this member is a
///
///
[JsonIgnore]
public bool IsBotDev
=> this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.VerifiedDeveloper);
///
/// Whether this member is a
///
///
[JsonIgnore]
public bool IsStaff
=> this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.Staff);
#endregion
///
/// Fetches the user from the API.
///
/// The user with fresh data from the API.
public async Task GetFromApiAsync()
=> await this.Discord.ApiClient.GetUserAsync(this.Id);
///
/// Whether this user is in a
///
///
///
/// DiscordGuild guild = await Client.GetGuildAsync(806675511555915806);
/// DiscordUser user = await Client.GetUserAsync(469957180968271873);
/// Console.WriteLine($"{user.Username} {(user.IsInGuild(guild) ? "is a" : "is not a")} member of {guild.Name}");
///
/// results to J_M_Lutra is a member of Project Nyaw~.
///
///
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "")]
public async Task IsInGuild(DiscordGuild guild)
{
try
{
var member = await guild.GetMemberAsync(this.Id);
return member is not null;
}
catch (NotFoundException)
{
return false;
}
}
///
/// Whether this user is not in a
///
///
///
public async Task IsNotInGuild(DiscordGuild guild)
=> !await this.IsInGuild(guild);
///
/// Returns the DiscordMember in the specified
///
/// The to get this user on.
/// The .
/// Thrown when the user is not part of the guild.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task ConvertToMember(DiscordGuild guild)
=> await guild.GetMemberAsync(this.Id);
///
/// Unbans this user from a guild.
///
/// Guild to unban this user from.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the user does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task UnbanAsync(DiscordGuild guild, string reason = null)
=> guild.UnbanMemberAsync(this, reason);
///
/// Gets this user's presence.
///
[JsonIgnore]
public DiscordPresence Presence
=> this.Discord is DiscordClient dc && dc.Presences.TryGetValue(this.Id, out var presence) ? presence : null;
///
/// Gets the user's avatar URL, in requested format and size.
///
/// Format of the avatar to get.
/// Maximum size of the avatar. Must be a power of two, minimum 16, maximum 2048.
/// URL of the user's avatar.
public string GetAvatarUrl(ImageFormat fmt, ushort size = 1024)
{
if (fmt == ImageFormat.Unknown)
throw new ArgumentException("You must specify valid image format.", nameof(fmt));
if (size < 16 || size > 2048)
throw new ArgumentOutOfRangeException(nameof(size));
var log = Math.Log(size, 2);
if (log < 4 || log > 11 || log % 1 != 0)
throw new ArgumentOutOfRangeException(nameof(size));
var sfmt = "";
sfmt = fmt switch
{
ImageFormat.Gif => "gif",
ImageFormat.Jpeg => "jpg",
ImageFormat.Png => "png",
ImageFormat.WebP => "webp",
ImageFormat.Auto => !string.IsNullOrWhiteSpace(this.AvatarHash) ? this.AvatarHash.StartsWith("a_") ? "gif" : "png" : "png",
_ => throw new ArgumentOutOfRangeException(nameof(fmt)),
};
var ssize = size.ToString(CultureInfo.InvariantCulture);
if (!string.IsNullOrWhiteSpace(this.AvatarHash))
{
var id = this.Id.ToString(CultureInfo.InvariantCulture);
return $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS}/{id}/{this.AvatarHash}.{sfmt}?size={ssize}";
}
else
{
var type = (this.DiscriminatorInt % 5).ToString(CultureInfo.InvariantCulture);
return $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.EMBED}{Endpoints.AVATARS}/{type}.{sfmt}?size={ssize}";
}
}
///
/// Returns a string representation of this user.
///
/// String representation of this user.
public override string ToString() => $"User {this.Id}; {this.Username}#{this.Discriminator}";
///
/// 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 DiscordUser);
///
/// Checks whether this is equal to another .
///
/// to compare to.
/// Whether the is equal to this .
public bool Equals(DiscordUser 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 user to compare.
/// Second user to compare.
/// Whether the two users are equal.
public static bool operator ==(DiscordUser e1, DiscordUser 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 user to compare.
/// Second user to compare.
/// Whether the two users are not equal.
public static bool operator !=(DiscordUser e1, DiscordUser e2)
=> !(e1 == e2);
}
///
/// Represents a user comparer.
///
internal class DiscordUserComparer : IEqualityComparer
{
///
/// Whether the users are equal.
///
/// The first user
/// The second user.
public bool Equals(DiscordUser x, DiscordUser y) => x.Equals(y);
///
/// Gets the hash code.
///
/// The user.
public int GetHashCode(DiscordUser obj) => obj.Id.GetHashCode();
}
diff --git a/DisCatSharp/Enums/User/PremiumType.cs b/DisCatSharp/Enums/User/PremiumType.cs
index 1957de3b5..d9460a440 100644
--- a/DisCatSharp/Enums/User/PremiumType.cs
+++ b/DisCatSharp/Enums/User/PremiumType.cs
@@ -1,44 +1,50 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
namespace DisCatSharp;
///
/// The type of Nitro subscription on a user's account.
///
public enum PremiumType
{
///
/// User does not have any perks.
///
None = 0,
///
/// Includes basic app perks like animated emojis and avatars.
///
NitroClassic = 1,
///
/// Includes all app perks.
///
- Nitro = 2
+ Nitro = 2,
+
+ ///
+ /// Includes lite app perks.
+ /// Unknown if it will be 3.
+ ///
+ NitroLite = 3
}
diff --git a/DisCatSharp/Net/Abstractions/Transport/TransportUser.cs b/DisCatSharp/Net/Abstractions/Transport/TransportUser.cs
index 4d342da07..3edd1cdc8 100644
--- a/DisCatSharp/Net/Abstractions/Transport/TransportUser.cs
+++ b/DisCatSharp/Net/Abstractions/Transport/TransportUser.cs
@@ -1,156 +1,163 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using Newtonsoft.Json;
namespace DisCatSharp.Net.Abstractions;
///
/// Represents a transport user.
///
internal class TransportUser
{
///
/// Gets the id.
///
[JsonProperty("id")]
public ulong Id { get; internal set; }
///
/// Gets the username.
///
[JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)]
public string Username { get; internal set; }
///
/// Gets or sets the discriminator.
///
[JsonProperty("discriminator", NullValueHandling = NullValueHandling.Ignore)]
internal string Discriminator { get; set; }
///
/// Gets the username with discriminator.
///
internal string UsernameWithDiscriminator
=> $"{this.Username}#{this.Discriminator}";
///
/// Gets the avatar hash.
///
[JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)]
public string AvatarHash { get; internal set; }
///
/// Gets the banner hash.
///
[JsonProperty("banner", NullValueHandling = NullValueHandling.Ignore)]
public string BannerHash { get; internal set; }
///
/// Gets the banner color.
///
[JsonProperty("accent_color")]
public int? BannerColor { get; internal set; }
///
/// Gets a value indicating whether is bot.
///
[JsonProperty("bot", NullValueHandling = NullValueHandling.Ignore)]
public bool IsBot { get; internal set; }
///
/// Gets a value indicating whether mfa enabled.
///
[JsonProperty("mfa_enabled", NullValueHandling = NullValueHandling.Ignore)]
public bool? MfaEnabled { get; internal set; }
///
/// Gets a value indicating whether verified.
///
[JsonProperty("verified", NullValueHandling = NullValueHandling.Ignore)]
public bool? Verified { get; internal set; }
///
/// Gets the email.
///
[JsonProperty("email", NullValueHandling = NullValueHandling.Ignore)]
public string Email { get; internal set; }
///
/// Gets the premium type.
///
[JsonProperty("premium_type", NullValueHandling = NullValueHandling.Ignore)]
public PremiumType? PremiumType { get; internal set; }
///
/// Gets the locale.
///
[JsonProperty("locale", NullValueHandling = NullValueHandling.Ignore)]
public string Locale { get; internal set; }
///
/// Gets the OAuth flags.
///
[JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)]
public UserFlags? OAuthFlags { get; internal set; }
///
/// Gets the flags.
///
[JsonProperty("public_flags", NullValueHandling = NullValueHandling.Ignore)]
public UserFlags? Flags { get; internal set; }
///
/// Gets the users bio.
/// This is not available to bots tho.
///
[JsonProperty("bio", NullValueHandling = NullValueHandling.Ignore)]
public string Bio { get; internal set; }
+ ///
+ /// Gets the users pronouns.
+ ///
+ [JsonProperty("pronouns", NullValueHandling = NullValueHandling.Ignore)]
+ public string Pronouns { get; internal set; }
+
///
/// Initializes a new instance of the class.
///
internal TransportUser() { }
///
/// Initializes a new instance of the class from an existing .
///
/// The other transport user.
internal TransportUser(TransportUser other)
{
this.Id = other.Id;
this.Username = other.Username;
this.Discriminator = other.Discriminator;
this.AvatarHash = other.AvatarHash;
this.BannerHash = other.BannerHash;
this.BannerColor = other.BannerColor;
this.IsBot = other.IsBot;
this.MfaEnabled = other.MfaEnabled;
this.Verified = other.Verified;
this.Email = other.Email;
this.PremiumType = other.PremiumType;
this.Locale = other.Locale;
this.Flags = other.Flags;
this.OAuthFlags = other.OAuthFlags;
this.Bio = other.Bio;
+ this.Pronouns = other.Pronouns;
}
}