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..4f1b7fe24
--- /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 application command.
+ ///
+ [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/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 24baf4ed0..631f5f8a1 100644
--- a/DisCatSharp/Net/Rest/DiscordApiClient.cs
+++ b/DisCatSharp/Net/Rest/DiscordApiClient.cs
@@ -1,5675 +1,5702 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.Net.Abstractions;
using DisCatSharp.Net.Serialization;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace DisCatSharp.Net;
///
/// Represents a discord api client.
///
public sealed class DiscordApiClient
{
///
/// The audit log reason header name.
///
private const string REASON_HEADER_NAME = "X-Audit-Log-Reason";
///
/// Gets the discord client.
///
internal BaseDiscordClient Discord { get; }
///
/// Gets the rest client.
///
internal RestClient Rest { get; }
///
/// Initializes a new instance of the class.
///
/// The client.
internal DiscordApiClient(BaseDiscordClient client)
{
this.Discord = client;
this.Rest = new RestClient(client);
}
///
/// Initializes a new instance of the class.
///
/// The proxy.
/// The timeout.
/// If true, use relative rate limit.
/// The logger.
internal DiscordApiClient(IWebProxy proxy, TimeSpan timeout, bool useRelativeRateLimit, ILogger logger) // This is for meta-clients, such as the webhook client
{
this.Rest = new RestClient(proxy, timeout, useRelativeRateLimit, logger);
}
///
/// Builds the query string.
///
/// The values.
/// Whether this query will be transmitted via POST.
private static string BuildQueryString(IDictionary values, bool post = false)
{
if (values == null || values.Count == 0)
return string.Empty;
var valsCollection = values.Select(xkvp =>
$"{WebUtility.UrlEncode(xkvp.Key)}={WebUtility.UrlEncode(xkvp.Value)}");
var vals = string.Join("&", valsCollection);
return !post ? $"?{vals}" : vals;
}
///
/// Prepares the message.
///
/// The msg_raw.
/// A DiscordMessage.
private DiscordMessage PrepareMessage(JToken msgRaw)
{
var author = msgRaw["author"].ToObject();
var ret = msgRaw.ToDiscordObject();
ret.Discord = this.Discord;
this.PopulateMessage(author, ret);
var referencedMsg = msgRaw["referenced_message"];
if (ret.MessageType == MessageType.Reply && !string.IsNullOrWhiteSpace(referencedMsg?.ToString()))
{
author = referencedMsg["author"].ToObject();
ret.ReferencedMessage.Discord = this.Discord;
this.PopulateMessage(author, ret.ReferencedMessage);
}
if (ret.Channel != null)
return ret;
var channel = !ret.GuildId.HasValue
? new DiscordDmChannel
{
Id = ret.ChannelId,
Discord = this.Discord,
Type = ChannelType.Private
}
: new DiscordChannel
{
Id = ret.ChannelId,
GuildId = ret.GuildId,
Discord = this.Discord
};
ret.Channel = channel;
return ret;
}
///
/// Populates the message.
///
/// The author.
/// The message.
private void PopulateMessage(TransportUser author, DiscordMessage ret)
{
var guild = ret.Channel?.Guild;
//If this is a webhook, it shouldn't be in the user cache.
if (author.IsBot && int.Parse(author.Discriminator) == 0)
{
ret.Author = new DiscordUser(author) { Discord = this.Discord };
}
else
{
if (!this.Discord.UserCache.TryGetValue(author.Id, out var usr))
{
this.Discord.UserCache[author.Id] = usr = new DiscordUser(author) { Discord = this.Discord };
}
if (guild != null)
{
if (!guild.Members.TryGetValue(author.Id, out var mbr))
mbr = new DiscordMember(usr) { Discord = this.Discord, GuildId = guild.Id };
ret.Author = mbr;
}
else
{
ret.Author = usr;
}
}
ret.PopulateMentions();
ret.ReactionsInternal ??= new List();
foreach (var xr in ret.ReactionsInternal)
xr.Emoji.Discord = this.Discord;
}
///
/// Executes a rest request.
///
/// The client.
/// The bucket.
/// The url.
/// The method.
/// The route.
/// The headers.
/// The payload.
/// The ratelimit wait override.
internal Task DoRequestAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, string payload = null, double? ratelimitWaitOverride = null)
{
var req = new RestRequest(client, bucket, url, method, route, headers, payload, ratelimitWaitOverride);
if (this.Discord != null)
this.Rest.ExecuteRequestAsync(req).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, $"Error while executing request. 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 ownerId, Optional splashb64,
Optional systemChannelId, Optional systemChannelFlags,
Optional publicUpdatesChannelId, Optional rulesChannelId, Optional description,
Optional bannerb64, Optional discoverySplashb64, Optional preferredLocale, Optional premiumProgressBarEnabled, string reason)
{
var pld = new RestGuildModifyPayload
{
Name = name,
VerificationLevel = verificationLevel,
DefaultMessageNotifications = defaultMessageNotifications,
MfaLevel = mfaLevel,
ExplicitContentFilter = explicitContentFilter,
AfkChannelId = afkChannelId,
AfkTimeout = afkTimeout,
IconBase64 = iconb64,
SplashBase64 = splashb64,
BannerBase64 = bannerb64,
DiscoverySplashBase64 = discoverySplashb64,
OwnerId = ownerId,
SystemChannelId = systemChannelId,
SystemChannelFlags = systemChannelFlags,
RulesChannelId = rulesChannelId,
PublicUpdatesChannelId = publicUpdatesChannelId,
PreferredLocale = preferredLocale,
Description = description,
PremiumProgressBarEnabled = premiumProgressBarEnabled
};
var headers = Utilities.GetBaseHeaders();
if (!string.IsNullOrWhiteSpace(reason))
headers.Add(REASON_HEADER_NAME, reason);
var route = $"{Endpoints.GUILDS}/:guild_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false);
var json = JObject.Parse(res.Response);
var rawMembers = (JArray)json["members"];
var guild = json.ToDiscordObject();
foreach (var r in guild.RolesInternal.Values)
r.GuildId = guild.Id;
if (this.Discord is DiscordClient dc)
await dc.OnGuildUpdateEventAsync(guild, rawMembers).ConfigureAwait(false);
return guild;
}
///
/// Modifies the guild community settings.
///
/// The guild id.
/// The guild features.
/// The rules channel id.
/// The public updates channel id.
/// The preferred locale.
/// The description.
/// The default message notifications.
/// The explicit content filter.
/// The verification level.
/// The reason.
internal async Task ModifyGuildCommunitySettingsAsync(ulong guildId, List features, Optional rulesChannelId, Optional publicUpdatesChannelId, string preferredLocale, string description, DefaultMessageNotifications defaultMessageNotifications, ExplicitContentFilter explicitContentFilter, VerificationLevel verificationLevel, string reason)
{
var pld = new RestGuildCommunityModifyPayload
{
VerificationLevel = verificationLevel,
DefaultMessageNotifications = defaultMessageNotifications,
ExplicitContentFilter = explicitContentFilter,
RulesChannelId = rulesChannelId,
PublicUpdatesChannelId = publicUpdatesChannelId,
PreferredLocale = preferredLocale,
Description = Optional.FromNullable(description),
Features = features
};
var headers = Utilities.GetBaseHeaders();
if (!string.IsNullOrWhiteSpace(reason))
headers.Add(REASON_HEADER_NAME, reason);
var route = $"{Endpoints.GUILDS}/:guild_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false);
var json = JObject.Parse(res.Response);
var rawMembers = (JArray)json["members"];
var guild = json.ToDiscordObject();
foreach (var r in guild.RolesInternal.Values)
r.GuildId = guild.Id;
if (this.Discord is DiscordClient dc)
await dc.OnGuildUpdateEventAsync(guild, rawMembers).ConfigureAwait(false);
return guild;
}
///