diff --git a/DisCatSharp/Clients/BaseDiscordClient.cs b/DisCatSharp/Clients/BaseDiscordClient.cs
index bd90a697a..e1289d178 100644
--- a/DisCatSharp/Clients/BaseDiscordClient.cs
+++ b/DisCatSharp/Clients/BaseDiscordClient.cs
@@ -1,322 +1,323 @@
// 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.
#pragma warning disable CS0618
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.Net;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
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.GetCurrentApplicationInfoAsync().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,
RequiresCodeGrant = tapp.BotRequiresCodeGrant,
IsPublic = tapp.IsPublicBot,
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();
return app;
}
///
/// Gets a list of voice regions.
///
/// Thrown when Discord is unable to process the request.
public Task> ListVoiceRegionsAsync()
=> 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 7903d0347..6388be96f 100644
--- a/DisCatSharp/Clients/DiscordClient.cs
+++ b/DisCatSharp/Clients/DiscordClient.cs
@@ -1,1530 +1,1544 @@
// 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._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.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;
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.GetApplicationInfoAsync(applicationId);
///
/// 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.
public async Task TryGetUserAsync(ulong userId, bool fetch = true)
{
try
{
return await this.GetUserAsync(userId, fetch).ConfigureAwait(false);
}
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.
public async Task TryGetChannelAsync(ulong id, bool fetch = true)
{
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.
public async Task TryGetThreadAsync(ulong id, bool fetch = true)
{
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.
public async Task TryGetGuildAsync(ulong id, bool? withCounts = null, bool fetch = true)
{
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.
public async Task TryGetGuildPreviewAsync(ulong id)
{
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.
public async Task TryGetGuildWidgetAsync(ulong id)
{
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.
public async Task TryGetInviteByCodeAsync(string code, bool? withCounts = null, bool? withExpiration = null, ulong? scheduledEventId = null)
{
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.
public async Task TryGetStickerAsync(ulong id)
{
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;
// 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);
///
/// 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.
public async Task TryGetWebhookAsync(ulong id)
{
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.
public async Task TryGetWebhookWithTokenAsync(ulong id, string token)
{
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)
{
if (this.Guilds == null)
return null;
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.AvatarDecorationHash = usr.AvatarDecorationHash;
old.ThemeColorsInternal = usr.ThemeColorsInternal;
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.AvatarDecorationHash = usr.AvatarDecorationHash;
old.ThemeColorsInternal = usr.ThemeColorsInternal;
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;
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;
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/Application/DiscordApplication.cs b/DisCatSharp/Entities/Application/DiscordApplication.cs
index 5c1cf14bf..baa176983 100644
--- a/DisCatSharp/Entities/Application/DiscordApplication.cs
+++ b/DisCatSharp/Entities/Application/DiscordApplication.cs
@@ -1,434 +1,440 @@
// 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.Net;
using Newtonsoft.Json;
namespace DisCatSharp.Entities;
///
/// Represents an OAuth2 application.
///
public sealed class DiscordApplication : DiscordMessageApplication, IEquatable
{
///
/// Gets the application's summary.
///
public string Summary { get; internal set; }
///
/// Gets the application's icon.
///
public override string Icon
=> !string.IsNullOrWhiteSpace(this.IconHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.APP_ICONS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.png?size=1024" : null;
///
/// Gets the application's icon hash.
///
public string IconHash { get; internal set; }
///
/// Gets the application's allowed RPC origins.
///
public IReadOnlyList RpcOrigins { get; internal set; }
///
/// Gets the application's flags.
///
public ApplicationFlags Flags { get; internal set; }
///
/// Gets the application's owners.
///
public List Owners { get; internal set; }
///
/// Gets whether this application's bot user requires code grant.
///
public bool? RequiresCodeGrant { get; internal set; }
///
/// Gets whether this bot application is public.
///
public bool? IsPublic { get; internal set; }
///
/// Gets the terms of service url of the application.
///
public string TermsOfServiceUrl { get; internal set; }
///
/// Gets the privacy policy url of the application.
///
public string PrivacyPolicyUrl { get; internal set; }
///
/// Gets the team name of the application.
///
public string TeamName { get; internal set; }
///
/// Gets the hash of the application's cover image.
///
public string CoverImageHash { get; internal set; }
///
/// Gets this application's cover image URL.
///
public override string CoverImageUrl
=> $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.APP_ICONS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.CoverImageHash}.png?size=1024";
///
/// Gets the team which owns this application.
///
public DiscordTeam Team { get; internal set; }
///
/// Gets the hex encoded key for verification in interactions and the GameSDK's GetTicket
///
public string VerifyKey { get; internal set; }
///
/// If this application is a game sold on Discord, this field will be the guild to which it has been linked
///
public ulong? GuildId { get; internal set; }
///
/// If this application is a game sold on Discord, this field will be the id of the "Game SKU" that is created, if exists
///
public ulong? PrimarySkuId { get; internal set; }
///
/// If this application is a game sold on Discord, this field will be the URL slug that links to the store page
///
public string Slug { get; internal set; }
///
/// Gets or sets a list of .
///
private IReadOnlyList _assets;
///
/// A custom url for the Add To Server button.
///
public string CustomInstallUrl { get; internal set; }
///
/// Install parameters for adding the application to a guild.
///
public DiscordApplicationInstallParams InstallParams { get; internal set; }
+ ///
+ /// The application's role connection verification entry point,
+ /// which when configured will render the app as a verification method in the guild role verification configuration.
+ ///
+ public string RoleConnectionsVerificationUrl { get; internal set; }
+
///
/// The application tags.
/// Not used atm.
///
public IReadOnlyList Tags { get; internal set; }
///
/// Whether the application is hooked.
///
public bool IsHook { get; internal set; }
///
/// Gets the application type.
/// Mostly null.
///
public string Type { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal DiscordApplication()
{ }
///
/// Gets the application's cover image URL, in requested format and size.
///
/// Format of the image to get.
/// Maximum size of the cover image. Must be a power of two, minimum 16, maximum 2048.
/// URL of the application's cover image.
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.Auto or ImageFormat.Png => "png",
ImageFormat.WebP => "webp",
_ => throw new ArgumentOutOfRangeException(nameof(fmt)),
};
var ssize = size.ToString(CultureInfo.InvariantCulture);
return !string.IsNullOrWhiteSpace(this.CoverImageHash)
? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.{sfmt}?size={ssize}"
: null;
}
///
/// Retrieves this application's assets.
///
/// This application's assets.
public async Task> GetAssetsAsync()
{
this._assets ??= await this.Discord.ApiClient.GetApplicationAssetsAsync(this).ConfigureAwait(false);
return this._assets;
}
///
/// Generates an oauth url for the application.
///
/// The permissions.
/// OAuth Url
public string GenerateBotOAuth(Permissions permissions = Permissions.None)
{
permissions &= PermissionMethods.FullPerms;
// hey look, it's not all annoying and blue :P
return new QueryUriBuilder($"{DiscordDomain.GetDomain(CoreDomain.Discord).Url}{Endpoints.OAUTH2}{Endpoints.AUTHORIZE}")
.AddParameter("client_id", this.Id.ToString(CultureInfo.InvariantCulture))
.AddParameter("scope", "bot")
.AddParameter("permissions", ((long)permissions).ToString(CultureInfo.InvariantCulture))
.ToString();
}
///
/// 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 DiscordApplication);
///
/// Checks whether this is equal to another .
///
/// to compare to.
/// Whether the is equal to this .
public bool Equals(DiscordApplication 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 application to compare.
/// Second application to compare.
/// Whether the two applications are equal.
public static bool operator ==(DiscordApplication e1, DiscordApplication 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 application to compare.
/// Second application to compare.
/// Whether the two applications are not equal.
public static bool operator !=(DiscordApplication e1, DiscordApplication e2)
=> !(e1 == e2);
}
///
/// Represents an discord asset.
///
public abstract class DiscordAsset
{
///
/// Gets the ID of this asset.
///
public virtual string Id { get; set; }
///
/// Gets the URL of this asset.
///
public abstract Uri Url { get; }
}
///
/// Represents an asset for an OAuth2 application.
///
public sealed class DiscordApplicationAsset : DiscordAsset, IEquatable
{
///
/// Gets the Discord client instance for this asset.
///
internal BaseDiscordClient Discord { get; set; }
///
/// Gets the asset's name.
///
[JsonProperty("name")]
public string Name { get; internal set; }
///
/// Gets the asset's type.
///
[JsonProperty("type")]
public ApplicationAssetType Type { get; internal set; }
///
/// Gets the application this asset belongs to.
///
public DiscordApplication Application { get; internal set; }
///
/// Gets the Url of this asset.
///
public override Uri Url
=> new($"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.APP_ASSETS}/{this.Application.Id.ToString(CultureInfo.InvariantCulture)}/{this.Id}.png");
///
/// Initializes a new instance of the class.
///
internal DiscordApplicationAsset()
{ }
///
/// Initializes a new instance of the class.
///
/// The app.
internal DiscordApplicationAsset(DiscordApplication app)
{
this.Discord = app.Discord;
}
///
/// 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 DiscordApplicationAsset);
///
/// Checks whether this is equal to another .
///
/// to compare to.
/// Whether the is equal to this .
public bool Equals(DiscordApplicationAsset 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 application asset to compare.
/// Second application asset to compare.
/// Whether the two application assets not equal.
public static bool operator ==(DiscordApplicationAsset e1, DiscordApplicationAsset 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 application asset to compare.
/// Second application asset to compare.
/// Whether the two application assets are not equal.
public static bool operator !=(DiscordApplicationAsset e1, DiscordApplicationAsset e2)
=> !(e1 == e2);
}
///
/// Represents an spotify asset.
///
public sealed class DiscordSpotifyAsset : DiscordAsset
{
///
/// Gets the URL of this asset.
///
public override Uri Url
=> this._url.Value;
private readonly Lazy _url;
///
/// Initializes a new instance of the class.
///
public DiscordSpotifyAsset()
{
this._url = new Lazy(() =>
{
var ids = this.Id.Split(':');
var id = ids[1];
return new Uri($"https://i.scdn.co/image/{id}");
});
}
}
///
/// Determines the type of the asset attached to the application.
///
public enum ApplicationAssetType : int
{
///
/// Unknown type. This indicates something went terribly wrong.
///
Unknown = 0,
///
/// This asset can be used as small image for rich presences.
///
SmallImage = 1,
///
/// This asset can be used as large image for rich presences.
///
LargeImage = 2
}
diff --git a/DisCatSharp/Entities/Application/DiscordApplicationRoleConnectionMetadata.cs b/DisCatSharp/Entities/Application/DiscordApplicationRoleConnectionMetadata.cs
new file mode 100644
index 000000000..8a092ff0d
--- /dev/null
+++ b/DisCatSharp/Entities/Application/DiscordApplicationRoleConnectionMetadata.cs
@@ -0,0 +1,145 @@
+// This file is part of the DisCatSharp project, based off DSharpPlus.
+//
+// Copyright (c) 2021-2022 AITSYS
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+using DisCatSharp.Enums;
+
+using Newtonsoft.Json;
+
+namespace DisCatSharp.Entities;
+
+///
+/// Represents a role connection metadata object that is registered to an application.
+///
+public sealed class DiscordApplicationRoleConnectionMetadata : SnowflakeObject, IEquatable
+{
+ ///
+ /// Gets the type of this role connection metadata object.
+ ///
+ [JsonProperty("type")]
+ public ApplicationRoleConnectionMetadataType Type { get; internal set; }
+
+ ///
+ /// The dictionary key for the metadata field.
+ /// Must be `a-z`, `0-9`, or `_` characters.
+ ///
+ [JsonProperty("key")]
+ public string Key { get; internal set; }
+
+ ///
+ /// Gets the name of the metadata field.
+ ///
+ [JsonProperty("name")]
+ public string Name { get; internal set; }
+
+ ///
+ /// Sets the name localizations.
+ ///
+ [JsonProperty("name_localizations", NullValueHandling = NullValueHandling.Ignore)]
+ internal Dictionary RawNameLocalizations { get; set; }
+
+ ///
+ /// Gets the name localizations.
+ ///
+ [JsonIgnore]
+ public DiscordApplicationCommandLocalization NameLocalizations
+ => new(this.RawNameLocalizations);
+
+ ///
+ /// Gets the description of the metadata field.
+ ///
+ [JsonProperty("description")]
+ public string Description { get; internal set; }
+
+ ///
+ /// Sets the description localizations.
+ ///
+ [JsonProperty("description_localizations", NullValueHandling = NullValueHandling.Ignore)]
+ internal Dictionary RawDescriptionLocalizations { get; set; }
+
+ ///
+ /// Gets the description localizations.
+ ///
+ [JsonIgnore]
+ public DiscordApplicationCommandLocalization DescriptionLocalizations
+ => new(this.RawDescriptionLocalizations);
+
+ ///
+ /// Creates a new instance of a .
+ ///
+ public DiscordApplicationRoleConnectionMetadata(
+ ApplicationRoleConnectionMetadataType type, string key, string name, string description,
+ DiscordApplicationCommandLocalization nameLocalizations = null, DiscordApplicationCommandLocalization descriptionLocalizations = null
+ )
+ {
+ this.Type = type;
+ this.Key = key;
+ this.Name = name;
+ this.Description = description;
+ this.RawNameLocalizations = nameLocalizations?.GetKeyValuePairs();
+ this.RawDescriptionLocalizations = descriptionLocalizations?.GetKeyValuePairs();
+ }
+
+ ///
+ /// Checks whether this object is equal to another object.
+ ///
+ /// The command to compare to.
+ /// Whether the command is equal to this .
+ public bool Equals(DiscordApplicationRoleConnectionMetadata other)
+ => this.Id == other.Id;
+
+ ///
+ /// Determines if two objects are equal.
+ ///
+ /// The first command object.
+ /// The second command object.
+ /// Whether the two objects are equal.
+ public static bool operator ==(DiscordApplicationRoleConnectionMetadata e1, DiscordApplicationRoleConnectionMetadata e2)
+ => e1.Equals(e2);
+
+ ///
+ /// Determines if two objects are not equal.
+ ///
+ /// The first command object.
+ /// The second command object.
+ /// Whether the two objects are not equal.
+ public static bool operator !=(DiscordApplicationRoleConnectionMetadata e1, DiscordApplicationRoleConnectionMetadata e2)
+ => !(e1 == e2);
+
+ ///
+ /// Determines if a is equal to the current .
+ ///
+ /// The object to compare to.
+ /// Whether the two objects are not equal.
+ public override bool Equals(object other)
+ => other is DiscordApplicationRoleConnectionMetadata dac && this.Equals(dac);
+
+ ///
+ /// Gets the hash code for this .
+ ///
+ /// The hash code for this .
+ public override int GetHashCode()
+ => this.Id.GetHashCode();
+}
diff --git a/DisCatSharp/Enums/Application/ApplicationRoleConnectionMetadataType.cs b/DisCatSharp/Enums/Application/ApplicationRoleConnectionMetadataType.cs
new file mode 100644
index 000000000..e40607699
--- /dev/null
+++ b/DisCatSharp/Enums/Application/ApplicationRoleConnectionMetadataType.cs
@@ -0,0 +1,69 @@
+// This file is part of the DisCatSharp project, based off DSharpPlus.
+//
+// Copyright (c) 2021-2022 AITSYS
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+namespace DisCatSharp.Enums;
+
+///
+/// Represents the type of an .
+///
+public enum ApplicationRoleConnectionMetadataType
+{
+ ///
+ /// The metadata value (`integer`) is less than or equal to the guild's configured value (`integer`).
+ ///
+ IntegerLessThanOrÈqual = 1,
+
+ ///
+ /// The metadata value (`integer`) is greater than or equal to the guild's configured value (`integer`).
+ ///
+ IntegerGreaterThanOrÈqual = 2,
+
+ ///
+ /// The metadata value (`integer`) is equal to the guild's configured value (`integer`).
+ ///
+ IntegerEqual = 3,
+
+ ///
+ /// The metadata value (`integer`) is not equal to the guild's configured value (`integer`).
+ ///
+ IntegerNotEqual = 4,
+
+ ///
+ /// The metadata value (`ISO8601 string`) is less than or equal to the guild's configured value (`integer`; `days before current date`).
+ ///
+ DatetimeLessThanOrÈqual = 5,
+
+ ///
+ /// The metadata value (`ISO8601 string`) is greater than or equal to the guild's configured value (`integer`; `days before current date`).
+ ///
+ DatetimeGreaterThanOrÈqual = 6,
+
+ ///
+ /// The metadata value (`integer`) is equal to the guild's configured value (`integer`; `1`).
+ ///
+ BooleanEqual = 7,
+
+ ///
+ /// The metadata value (`integer`) is not equal to the guild's configured value (`integer`; `1`).
+ ///
+ BooleanNotEqual = 8
+}
diff --git a/DisCatSharp/Enums/OAuth.cs b/DisCatSharp/Enums/OAuth.cs
index 9772d17e5..234276da7 100644
--- a/DisCatSharp/Enums/OAuth.cs
+++ b/DisCatSharp/Enums/OAuth.cs
@@ -1,117 +1,122 @@
// 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.
// ReSharper disable InconsistentNaming
namespace DisCatSharp.Enums;
///
/// The oauth scopes.
///
public static class OAuth
{
///
/// The default scopes for bots.
///
private const string BOT_DEFAULT = "bot applications.commands"; // applications.commands.permissions.update
///
/// The bot minimal scopes.
///
private const string BOT_MINIMAL = "bot applications.commands";
///
/// The bot only scope.
///
private const string BOT_ONLY = "bot";
///
/// The basic identify scopes.
///
private const string IDENTIFY_BASIC = "identify email";
///
/// The extended identify scopes.
///
private const string IDENTIFY_EXTENDED = "identify email guilds guilds.members.read connections";
///
- /// All scopes for bots and identify.
+ /// The role connection scope.
///
- private const string ALL = BOT_DEFAULT + " " + IDENTIFY_EXTENDED;
+ private const string ROLE_CONNECTIONS_WRITE = "role_connections.write";
///
- /// The oauth scope.
+ /// All scopes for bots and identify.
///
-
+ private const string ALL = BOT_DEFAULT + " " + IDENTIFY_EXTENDED + " " + ROLE_CONNECTIONS_WRITE;
///
/// Resolves the scopes.
///
/// The scope.
/// A string representing the scopes.
public static string ResolveScopes(OAuthScopes scope) =>
scope switch
{
OAuthScopes.BOT_DEFAULT => BOT_DEFAULT,
OAuthScopes.BOT_MINIMAL => BOT_MINIMAL,
OAuthScopes.BOT_ONLY => BOT_ONLY,
OAuthScopes.IDENTIFY_BASIC => IDENTIFY_BASIC,
OAuthScopes.IDENTIFY_EXTENDED => IDENTIFY_EXTENDED,
OAuthScopes.ALL => ALL,
_ => BOT_DEFAULT,
};
}
///
/// The oauth scopes.
///
public enum OAuthScopes
{
///
/// Scopes: bot applications.commands (Excluding applications.commands.permissions.update for now)
///
BOT_DEFAULT = 0,
///
/// Scopes: bot applications.commands
///
BOT_MINIMAL = 1,
///
/// Scopes: bot
///
BOT_ONLY = 2,
///
/// Scopes: identify email
///
IDENTIFY_BASIC = 3,
///
/// Scopes: identify email guilds connections
///
IDENTIFY_EXTENDED = 4,
///
- /// Scopes: bot applications.commands applications.commands.permissions.update identify email guilds connections
+ /// Scopes: bot applications.commands applications.commands.permissions.update identify email guilds connections role_connections.write
+ ///
+ ALL = 5,
+
+ ///
+ /// Scopes: role_connections.write
///
- ALL = 5
+ ROLE_CONNECTIONS_WRITE = 6
}
diff --git a/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs b/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs
index 99aa7c3e1..7d04ad5f9 100644
--- a/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs
+++ b/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs
@@ -1,240 +1,282 @@
// 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.Collections.Generic;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using Newtonsoft.Json;
namespace DisCatSharp.Net.Abstractions;
///
/// Represents a application command create payload.
///
internal class RestApplicationCommandCreatePayload
{
///
/// Gets the type.
///
[JsonProperty("type")]
public ApplicationCommandType Type { get; set; }
///
/// Gets the name.
///
[JsonProperty("name")]
public string Name { get; set; }
///
/// Gets the name localizations.
///
[JsonProperty("name_localizations", NullValueHandling = NullValueHandling.Ignore)]
public Optional> NameLocalizations { get; set; }
///
/// Gets the description.
///
[JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)]
public string Description { get; set; }
///
/// Gets the description localizations.
///
[JsonProperty("description_localizations", NullValueHandling = NullValueHandling.Ignore)]
public Optional> DescriptionLocalizations { get; set; }
///
/// Gets the options.
///
[JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)]
public IEnumerable Options { get; set; }
///
/// Whether the command is allowed for everyone.
///
[JsonProperty("default_permission", NullValueHandling = NullValueHandling.Include)]
public bool? DefaultPermission { get; set; } = null;
///
/// The command needed permissions.
///
[JsonProperty("default_member_permissions", NullValueHandling = NullValueHandling.Include)]
public Permissions? DefaultMemberPermission { get; set; }
///
/// Whether the command is allowed for dms.
///
[JsonProperty("dm_permission", NullValueHandling = NullValueHandling.Include)]
public bool? DmPermission { get; set; }
///
/// Whether the command is marked as NSFW.
///
[JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)]
public bool Nsfw { get; set; }
}
///
/// Represents a application command edit payload.
///
internal class RestApplicationCommandEditPayload
{
///
/// Gets the name.
///
[JsonProperty("name")]
public Optional Name { get; set; }
///
/// Gets the name localizations.
///
[JsonProperty("name_localizations")]
public Optional> NameLocalizations { get; set; }
///
/// Gets the description.
///
[JsonProperty("description")]
public Optional Description { get; set; }
///
/// Gets the description localizations.
///
[JsonProperty("description_localizations")]
public Optional> DescriptionLocalizations { get; set; }
///
/// Gets the options.
///
[JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)]
public Optional> Options { get; set; }
///
/// The command needed permissions.
///
[JsonProperty("default_member_permissions", NullValueHandling = NullValueHandling.Include)]
public Optional DefaultMemberPermission { get; set; }
///
/// Whether the command is allowed for dms.
///
[JsonProperty("dm_permission", NullValueHandling = NullValueHandling.Include)]
public Optional DmPermission { get; set; }
///
/// Whether the command is marked as NSFW.
///
[JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)]
public Optional Nsfw { get; set; }
}
///
/// Represents a interaction response payload.
///
internal class RestInteractionResponsePayload
{
///
/// Gets the type.
///
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
public InteractionResponseType Type { get; set; }
///
/// Gets the data.
///
[JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)]
public DiscordInteractionApplicationCommandCallbackData Data { get; set; }
///
/// Gets the attachments.
///
[JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)]
public List Attachments { get; set; }
}
///
/// Represents a interaction response payload.
///
internal class RestInteractionModalResponsePayload
{
///
/// Gets the type.
///
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
public InteractionResponseType Type { get; set; }
///
/// Gets the data.
///
[JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)]
public DiscordInteractionApplicationCommandModalCallbackData Data { get; set; }
}
///
/// Represents a followup message create payload.
///
internal class RestFollowupMessageCreatePayload
{
///
/// Gets the content.
///
[JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)]
public string Content { get; set; }
///
/// Get whether the message is tts.
///
[JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)]
public bool? IsTts { get; set; }
///
/// Gets the embeds.
///
[JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)]
public IEnumerable Embeds { get; set; }
///
/// Gets the mentions.
///
[JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)]
public DiscordMentions Mentions { get; set; }
///
/// Gets the flags.
///
[JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)]
public int? Flags { get; set; }
///
/// Gets the components.
///
[JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)]
public IReadOnlyCollection Components { get; set; }
///
/// Gets attachments.
///
[JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)]
public List Attachments { get; set; }
}
+
+///
+/// Represents a role connection metadata payload.
+///
+internal class RestApplicationRoleConnectionMetadataPayload
+{
+ ///
+ /// Gets the metadata type.
+ ///
+ [JsonProperty("type")]
+ public ApplicationRoleConnectionMetadataType Type { get; set; }
+
+ ///
+ /// Gets the metadata key.
+ ///
+ [JsonProperty("key")]
+ public string Key { get; set; }
+
+ ///
+ /// Gets the metadata name.
+ ///
+ [JsonProperty("name")]
+ public string Name { get; set; }
+
+ ///
+ /// Gets the metadata description.
+ ///
+ [JsonProperty("description")]
+ public string Description { get; set; }
+
+ ///
+ /// Gets the metadata name translations.
+ ///
+ [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)]
+ public Dictionary NameLocalizations { get; set; }
+
+ ///
+ /// Gets the metadata description localizations.
+ ///
+ [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)]
+ public Dictionary DescriptionLocalizations { get; set; }
+}
diff --git a/DisCatSharp/Net/Abstractions/Transport/TransportApplication.cs b/DisCatSharp/Net/Abstractions/Transport/TransportApplication.cs
index 5682a9623..8b3e493c1 100644
--- a/DisCatSharp/Net/Abstractions/Transport/TransportApplication.cs
+++ b/DisCatSharp/Net/Abstractions/Transport/TransportApplication.cs
@@ -1,180 +1,186 @@
// 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.Collections.Generic;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using Newtonsoft.Json;
namespace DisCatSharp.Net.Abstractions;
///
/// The transport application.
///
internal sealed class TransportApplication
{
///
/// Gets or sets the id.
///
[JsonProperty("id", NullValueHandling = NullValueHandling.Include)]
public ulong Id { get; set; }
///
/// Gets or sets the name.
///
[JsonProperty("name", NullValueHandling = NullValueHandling.Include)]
public string Name { get; set; }
///
/// Gets or sets the icon hash.
///
[JsonProperty("icon", NullValueHandling = NullValueHandling.Include)]
public string IconHash { get; set; }
///
/// Gets or sets the description.
///
[JsonProperty("description", NullValueHandling = NullValueHandling.Include)]
public string Description { get; set; }
///
/// Gets or sets the summary.
///
[JsonProperty("summary", NullValueHandling = NullValueHandling.Include)]
public string Summary { get; set; }
///
/// Whether the bot is public.
///
[JsonProperty("bot_public", NullValueHandling = NullValueHandling.Include)]
public bool IsPublicBot { get; set; }
///
/// Gets or sets the flags.
///
[JsonProperty("flags", NullValueHandling = NullValueHandling.Include)]
public ApplicationFlags Flags { get; set; }
///
/// Gets or sets the terms of service url.
///
[JsonProperty("terms_of_service_url", NullValueHandling = NullValueHandling.Include)]
public string TermsOfServiceUrl { get; set; }
///
/// Gets or sets the privacy policy url.
///
[JsonProperty("privacy_policy_url", NullValueHandling = NullValueHandling.Include)]
public string PrivacyPolicyUrl { get; set; }
///
/// Gets or sets a value indicating whether the bot requires code grant.
///
[JsonProperty("bot_require_code_grant", NullValueHandling = NullValueHandling.Include)]
public bool BotRequiresCodeGrant { get; set; }
///
/// Gets or sets a value indicating whether the bot is a hook.
///
[JsonProperty("hook", NullValueHandling = NullValueHandling.Ignore)]
public bool IsHook { get; set; }
///
/// Gets or sets a value indicating whether the bot requires code grant.
///
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
public string Type { get; set; }
///
/// Gets or sets the rpc origins.
///
[JsonProperty("rpc_origins", NullValueHandling = NullValueHandling.Ignore)]
public IList RpcOrigins { get; set; }
///
/// Gets or sets the owner.
///
[JsonProperty("owner", NullValueHandling = NullValueHandling.Include)]
public TransportUser Owner { get; set; }
///
/// Gets or sets the team.
///
[JsonProperty("team", NullValueHandling = NullValueHandling.Include)]
public TransportTeam Team { get; set; }
///
/// Gets or sets the verify key.
///
[JsonProperty("verify_key", NullValueHandling = NullValueHandling.Include)]
public Optional VerifyKey { get; set; }
///
/// Gets or sets the guild id.
///
[JsonProperty("guild_id")]
public Optional GuildId { get; set; }
///
/// Gets or sets the primary sku id.
///
[JsonProperty("primary_sku_id")]
public Optional PrimarySkuId { get; set; }
///
/// Gets or sets the slug.
///
[JsonProperty("slug")]
public Optional Slug { get; set; }
///
/// Gets or sets the cover image hash.
///
[JsonProperty("cover_image")]
public Optional CoverImageHash { get; set; }
///
/// Gets or sets the custom install url.
///
[JsonProperty("custom_install_url")]
public string CustomInstallUrl { get; set; }
///
/// Gets or sets the install params.
///
[JsonProperty("install_params", NullValueHandling = NullValueHandling.Include)]
public DiscordApplicationInstallParams InstallParams { get; set; }
+ ///
+ /// Gets or sets the role connection verification entry point.
+ ///
+ [JsonProperty("role_connections_verification_url")]
+ public string RoleConnectionsVerificationUrl { get; set; }
+
///
/// Gets or sets the tags.
///
[JsonProperty("tags", NullValueHandling = NullValueHandling.Include)]
public List Tags { get; set; }
///
/// Initializes a new instance of the class.
///
internal TransportApplication()
{ }
}
diff --git a/DisCatSharp/Net/Rest/DiscordApiClient.cs b/DisCatSharp/Net/Rest/DiscordApiClient.cs
index 98f173e38..0807a05cd 100644
--- a/DisCatSharp/Net/Rest/DiscordApiClient.cs
+++ b/DisCatSharp/Net/Rest/DiscordApiClient.cs
@@ -1,5675 +1,5728 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.Net.Abstractions;
using DisCatSharp.Net.Serialization;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
+using static System.Net.Mime.MediaTypeNames;
+
namespace DisCatSharp.Net;
///
/// Represents a discord api client.
///
public sealed class DiscordApiClient
{
///
/// The audit log reason header name.
///
private const string REASON_HEADER_NAME = "X-Audit-Log-Reason";
///
/// Gets the discord client.
///
internal BaseDiscordClient Discord { get; }
///
/// Gets the rest client.
///
internal RestClient Rest { get; }
///
/// Initializes a new instance of the class.
///
/// The client.
internal DiscordApiClient(BaseDiscordClient client)
{
this.Discord = client;
this.Rest = new RestClient(client);
}
///
/// Initializes a new instance of the class.
///
/// The proxy.
/// The timeout.
/// If true, use relative rate limit.
/// The logger.
internal DiscordApiClient(IWebProxy proxy, TimeSpan timeout, bool useRelativeRateLimit, ILogger logger) // This is for meta-clients, such as the webhook client
{
this.Rest = new RestClient(proxy, timeout, useRelativeRateLimit, logger);
}
///
/// Builds the query string.
///
/// The values.
/// Whether this query will be transmitted via POST.
private static string BuildQueryString(IDictionary values, bool post = false)
{
if (values == null || values.Count == 0)
return string.Empty;
var valsCollection = values.Select(xkvp =>
$"{WebUtility.UrlEncode(xkvp.Key)}={WebUtility.UrlEncode(xkvp.Value)}");
var vals = string.Join("&", valsCollection);
return !post ? $"?{vals}" : vals;
}
///
/// Prepares the message.
///
/// The msg_raw.
/// A DiscordMessage.
private DiscordMessage PrepareMessage(JToken msgRaw)
{
var author = msgRaw["author"].ToObject();
var ret = msgRaw.ToDiscordObject();
ret.Discord = this.Discord;
this.PopulateMessage(author, ret);
var referencedMsg = msgRaw["referenced_message"];
if (ret.MessageType == MessageType.Reply && !string.IsNullOrWhiteSpace(referencedMsg?.ToString()))
{
author = referencedMsg["author"].ToObject();
ret.ReferencedMessage.Discord = this.Discord;
this.PopulateMessage(author, ret.ReferencedMessage);
}
if (ret.Channel != null)
return ret;
var channel = !ret.GuildId.HasValue
? new DiscordDmChannel
{
Id = ret.ChannelId,
Discord = this.Discord,
Type = ChannelType.Private
}
: new DiscordChannel
{
Id = ret.ChannelId,
GuildId = ret.GuildId,
Discord = this.Discord
};
ret.Channel = channel;
return ret;
}
///
/// Populates the message.
///
/// The author.
/// The message.
private void PopulateMessage(TransportUser author, DiscordMessage ret)
{
var guild = ret.Channel?.Guild;
//If this is a webhook, it shouldn't be in the user cache.
if (author.IsBot && int.Parse(author.Discriminator) == 0)
{
ret.Author = new DiscordUser(author) { Discord = this.Discord };
}
else
{
if (!this.Discord.UserCache.TryGetValue(author.Id, out var usr))
{
this.Discord.UserCache[author.Id] = usr = new DiscordUser(author) { Discord = this.Discord };
}
if (guild != null)
{
if (!guild.Members.TryGetValue(author.Id, out var mbr))
mbr = new DiscordMember(usr) { Discord = this.Discord, GuildId = guild.Id };
ret.Author = mbr;
}
else
{
ret.Author = usr;
}
}
ret.PopulateMentions();
ret.ReactionsInternal ??= new List();
foreach (var xr in ret.ReactionsInternal)
xr.Emoji.Discord = this.Discord;
}
///
/// Executes a rest request.
///
/// The client.
/// The bucket.
/// The url.
/// The method.
/// The route.
/// The headers.
/// The payload.
/// The ratelimit wait override.
internal Task DoRequestAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, string payload = null, double? ratelimitWaitOverride = null)
{
var req = new RestRequest(client, bucket, url, method, route, headers, payload, ratelimitWaitOverride);
if (this.Discord != null)
this.Rest.ExecuteRequestAsync(req).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, $"Error while executing request. Url: {url.AbsoluteUri}");
else
_ = this.Rest.ExecuteRequestAsync(req);
return req.WaitForCompletionAsync();
}
///
/// Executes a multipart rest request for stickers.
///
/// The client.
/// The bucket.
/// The url.
/// The method.
/// The route.
/// The headers.
/// The file.
/// The sticker name.
/// The sticker tag.
/// The sticker description.
/// The ratelimit wait override.
private Task DoStickerMultipartAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null,
DiscordMessageFile file = null, string name = "", string tags = "", string description = "", double? ratelimitWaitOverride = null)
{
var req = new MultipartStickerWebRequest(client, bucket, url, method, route, headers, file, name, tags, description, ratelimitWaitOverride);
if (this.Discord != null)
this.Rest.ExecuteRequestAsync(req).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request");
else
_ = this.Rest.ExecuteRequestAsync(req);
return req.WaitForCompletionAsync();
}
///
/// Executes a multipart request.
///
/// The client.
/// The bucket.
/// The url.
/// The method.
/// The route.
/// The headers.
/// The values.
/// The files.
/// The ratelimit wait override.
private Task DoMultipartAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, IReadOnlyDictionary values = null,
IReadOnlyCollection files = null, double? ratelimitWaitOverride = null)
{
var req = new MultipartWebRequest(client, bucket, url, method, route, headers, values, files, ratelimitWaitOverride);
if (this.Discord != null)
this.Rest.ExecuteRequestAsync(req).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request");
else
_ = this.Rest.ExecuteRequestAsync(req);
return req.WaitForCompletionAsync();
}
#region Guild
///
/// Searches the members async.
///
/// The guild_id.
/// The name.
/// The limit.
internal async Task> SearchMembersAsync(ulong guildId, string name, int? limit)
{
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}{Endpoints.SEARCH}";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path);
var querydict = new Dictionary
{
["query"] = name,
["limit"] = limit.ToString()
};
var url = Utilities.GetApiUriFor(path, BuildQueryString(querydict), this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false);
var json = JArray.Parse(res.Response);
var tms = json.ToObject>();
var mbrs = new List();
foreach (var xtm in tms)
{
var usr = new DiscordUser(xtm.User) { Discord = this.Discord };
this.Discord.UserCache.AddOrUpdate(xtm.User.Id, usr, (id, old) =>
{
old.Username = usr.Username;
old.Discord = usr.Discord;
old.AvatarHash = usr.AvatarHash;
return old;
});
mbrs.Add(new DiscordMember(xtm) { Discord = this.Discord, GuildId = guildId });
}
return mbrs;
}
///
/// Gets the guild ban async.
///
/// The guild_id.
/// The user_id.
internal async Task GetGuildBanAsync(ulong guildId, ulong userId)
{
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId, user_id = userId}, out var path);
var uri = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, uri, RestRequestMethod.GET, route).ConfigureAwait(false);
var json = JObject.Parse(res.Response);
var ban = json.ToObject();
return ban;
}
///
/// Creates the guild async.
///
/// The name.
/// The region_id.
/// The iconb64.
/// The verification_level.
/// The default_message_notifications.
/// The system_channel_flags.
internal async Task CreateGuildAsync(string name, string regionId, Optional iconb64, VerificationLevel? verificationLevel,
DefaultMessageNotifications? defaultMessageNotifications, SystemChannelFlags? systemChannelFlags)
{
var pld = new RestGuildCreatePayload
{
Name = name,
RegionId = regionId,
DefaultMessageNotifications = defaultMessageNotifications,
VerificationLevel = verificationLevel,
IconBase64 = iconb64,
SystemChannelFlags = systemChannelFlags
};
var route = $"{Endpoints.GUILDS}";
var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false);
var json = JObject.Parse(res.Response);
var rawMembers = (JArray)json["members"];
var guild = json.ToDiscordObject();
if (this.Discord is DiscordClient dc)
await dc.OnGuildCreateEventAsync(guild, rawMembers, null).ConfigureAwait(false);
return guild;
}
///
/// Creates the guild from template async.
///
/// The template_code.
/// The name.
/// The iconb64.
internal async Task CreateGuildFromTemplateAsync(string templateCode, string name, Optional iconb64)
{
var pld = new RestGuildCreateFromTemplatePayload
{
Name = name,
IconBase64 = iconb64
};
var route = $"{Endpoints.GUILDS}{Endpoints.TEMPLATES}/:template_code";
var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {template_code = templateCode }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false);
var json = JObject.Parse(res.Response);
var rawMembers = (JArray)json["members"];
var guild = json.ToDiscordObject();
if (this.Discord is DiscordClient dc)
await dc.OnGuildCreateEventAsync(guild, rawMembers, null).ConfigureAwait(false);
return guild;
}
///
/// Deletes the guild async.
///
/// The guild_id.
internal async Task DeleteGuildAsync(ulong guildId)
{
var route = $"{Endpoints.GUILDS}/:guild_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {guild_id = guildId }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route).ConfigureAwait(false);
if (this.Discord is DiscordClient dc)
{
var gld = dc.GuildsInternal[guildId];
await dc.OnGuildDeleteEventAsync(gld).ConfigureAwait(false);
}
}
///
/// Modifies the guild.
///
/// The guild id.
/// The name.
/// The verification level.
/// The default message notifications.
/// The mfa level.
/// The explicit content filter.
/// The afk channel id.
/// The afk timeout.
/// The iconb64.
/// The owner id.
/// The splashb64.
/// The system channel id.
/// The system channel flags.
/// The public updates channel id.
/// The rules channel id.
/// The description.
/// The banner base64.
/// The discovery base64.
/// The preferred locale.
/// Whether the premium progress bar should be enabled.
/// The reason.
internal async Task ModifyGuildAsync(ulong guildId, Optional name, Optional verificationLevel,
Optional defaultMessageNotifications, Optional mfaLevel,
Optional explicitContentFilter, Optional afkChannelId,
Optional afkTimeout, Optional iconb64, Optional