feat: enhance application management with deauthorization and authorized applications retrieval

This commit is contained in:
Elysia
2025-07-12 20:43:40 +07:00
parent ce747eb5a0
commit fdc67d0eff
5 changed files with 612 additions and 12 deletions

View File

@@ -32,6 +32,7 @@ const StickerPack = require('../structures/StickerPack');
const VoiceRegion = require('../structures/VoiceRegion'); const VoiceRegion = require('../structures/VoiceRegion');
const Webhook = require('../structures/Webhook'); const Webhook = require('../structures/Webhook');
const Widget = require('../structures/Widget'); const Widget = require('../structures/Widget');
const Application = require('../structures/interfaces/Application');
const { Events, Status } = require('../util/Constants'); const { Events, Status } = require('../util/Constants');
const DataResolver = require('../util/DataResolver'); const DataResolver = require('../util/DataResolver');
const Intents = require('../util/Intents'); const Intents = require('../util/Intents');
@@ -798,15 +799,49 @@ class Client extends BaseClient {
} }
/** /**
* Deauthorize an application. * Deauthorizes an application or token.
* @param {Snowflake} applicationId Discord Application id * @param {Snowflake} id - The ID of the Discord Application or Token.
* @returns {Promise<void>} * @param {'application' | 'token'} [type='application'] - The type of the ID provided. Defaults to 'application'.
* @returns {Promise<void>} A promise that resolves when the deauthorization is complete.
*/ */
deauthorize(applicationId) { deauthorize(id, type = 'application') {
if (type === 'application') {
return this.api.oauth2.tokens return this.api.oauth2.tokens
.get() .get()
.then(data => data.find(o => o.application.id == applicationId)) .then(data => data.find(o => o.application.id == id))
.then(o => this.api.oauth2.tokens(o.id).delete()); .then(o => this.api.oauth2.tokens(o.id).delete());
} else {
return this.api.oauth2.tokens(id).delete();
}
}
/**
* @typedef {Object} AuthorizedApplicationData
* @property {Application} application - The application object.
* @property {Snowflake} authorizedApplicationId - The ID of the OAuth2 token.
* @property {string[]} scopes - The scopes that were granted to this token.
* @property {function(): Promise<void>} deauthorize - Function to revoke this token.
*/
/**
* Retrieves the list of authorized applications (OAuth2 tokens).
* @returns {Promise<Collection<Snowflake, AuthorizedApplicationData>>}
*/
authorizedApplications() {
return this.api.oauth2.tokens.get().then(data => {
const results = new Collection();
for (const o of data) {
const application = new Application(this, o.application);
const data = {
application,
authorizedApplicationId: o.id,
scopes: o.scopes,
deauthorize: () => this.deauthorize(o.id, 'token'),
};
results.set(o.application.id, data);
}
return results;
});
} }
/** /**

View File

@@ -53,7 +53,7 @@ class SessionManager extends CachedManager {
/** /**
* Get the current session of the client. * Get the current session of the client.
* You must call `fetch()` first to populate the cache. * You must call `fetch()` first to populate the cache.
* @returns {?Session} * @type {?Session}
*/ */
get currentSession() { get currentSession() {
if (!this.currentSessionIdHash) { if (!this.currentSessionIdHash) {

View File

@@ -1,7 +1,7 @@
'use strict'; 'use strict';
const process = require('node:process'); const process = require('node:process');
const { ApplicationFlags } = require('../../util/ApplicationFlags'); const ApplicationFlags = require('../../util/ApplicationFlags');
const { ClientApplicationAssetTypes, Endpoints } = require('../../util/Constants'); const { ClientApplicationAssetTypes, Endpoints } = require('../../util/Constants');
const Permissions = require('../../util/Permissions'); const Permissions = require('../../util/Permissions');
const SnowflakeUtil = require('../../util/SnowflakeUtil'); const SnowflakeUtil = require('../../util/SnowflakeUtil');
@@ -101,7 +101,6 @@ class Application extends Base {
this.roleConnectionsVerificationURL ??= null; this.roleConnectionsVerificationURL ??= null;
} }
// ClientApplication
/** /**
* The tags this application has (max of 5) * The tags this application has (max of 5)
* @type {string[]} * @type {string[]}
@@ -182,6 +181,7 @@ class Application extends Base {
if ('bot_require_code_grant' in data) { if ('bot_require_code_grant' in data) {
/** /**
* If this application's bot requires a code grant when using the OAuth2 flow * If this application's bot requires a code grant when using the OAuth2 flow
* @deprecated
* @type {?boolean} * @type {?boolean}
*/ */
this.botRequireCodeGrant = data.bot_require_code_grant; this.botRequireCodeGrant = data.bot_require_code_grant;
@@ -192,6 +192,7 @@ class Application extends Base {
if ('bot_public' in data) { if ('bot_public' in data) {
/** /**
* If this application's bot is public * If this application's bot is public
* @deprecated
* @type {?boolean} * @type {?boolean}
*/ */
this.botPublic = data.bot_public; this.botPublic = data.bot_public;
@@ -208,6 +209,487 @@ class Application extends Base {
: data.owner : data.owner
? this.client.users._add(data.owner) ? this.client.users._add(data.owner)
: this.owner ?? null; : this.owner ?? null;
if ('splash' in data) {
/**
* The application's splash image hash
* @type {?string}
*/
this.splash = data.splash;
} else {
this.splash ??= null;
}
if ('type' in data) {
/**
* The type of the application
* @type {number}
* @see {@link https://docs.discord.food/resources/application#application-type}
*/
this.type = data.type;
} else {
this.type = null;
}
if ('primary_sku_id' in data) {
/**
* The ID of the application's primary SKU (game, application subscription, etc.)
* @type {?Snowflake}
*/
this.primarySkuId = data.primary_sku_id;
} else {
this.primarySkuId = null;
}
if ('eula_id' in data) {
/**
* The ID of the application's EULA
* @type {?Snowflake}
*/
this.eulaId = data.eula_id;
} else {
this.eulaId = null;
}
if ('slug' in data) {
/**
* The URL slug that links to the primary store page of the application
* @type {?string}
*/
this.slug = data.slug;
} else {
this.slug = null;
}
if ('aliases' in data) {
/**
* Other names the application's game is associated with
* @type {string[]}
*/
this.aliases = data.aliases;
} else {
this.aliases = [];
}
if ('executables' in data) {
/**
* @typedef {Object} ApplicationExecutable
* @property {string} os - The operating system the executable can be found on
* @property {string} name - The name of the executable
* @property {boolean} is_launcher - Whether the executable is for a game launcher
*/
/**
* The unique executables of the application's game
* @type {?ApplicationExecutable[]}
*/
this.executables = data.executables;
} else {
this.executables ??= null;
}
if ('third_party_skus' in data) {
/**
* @typedef {Object} ApplicationSku
* @property {?Snowflake} id - The ID of the game
* @property {?string} sku - The SKU of the game
* @property {string} distributor - The distributor of the game
*/
/**
* The third party SKUs of the application's game
* @type {?ApplicationSku[]}
*/
this.thirdPartySkus = data.third_party_skus;
} else {
this.thirdPartySkus ??= null;
}
if ('hook' in data) {
/**
* Whether the Discord client is allowed to hook into the application's game directly
* @type {?boolean}
*/
this.hook = data.hook;
} else {
this.hook ??= null;
}
if ('overlay' in data) {
/**
* Whether the application's game supports the Discord overlay (default false)
* @type {?boolean}
*/
this.overlay = data.overlay;
} else {
this.overlay ??= null;
}
if ('overlay_methods' in data) {
/**
* The methods of overlaying that the application's game supports
* @type {?number}
*/
this.overlayMethods = data.overlay_methods;
} else {
this.overlayMethods ??= null;
}
if ('overlay_warn' in data) {
/**
* Whether the Discord overlay is known to be problematic with this application's game (default false)
* @type {?boolean}
*/
this.overlayWarn = data.overlay_warn;
} else {
this.overlayWarn ??= null;
}
if ('overlay_compatibility_hook' in data) {
/**
* Whether to use the compatibility hook for the overlay (default false)
* @type {?boolean}
*/
this.overlayCompatibilityHook = data.overlay_compatibility_hook;
} else {
this.overlayCompatibilityHook ??= null;
}
if ('bot' in data) {
/**
* The bot attached to this application
* @type {?User}
*/
this.bot = data.bot;
} else {
this.bot ??= null;
}
if ('developers' in data) {
/**
* @typedef {Object} Company
* @property {?Snowflake} id - The ID of the company
* @property {?string} name - The name of the company
*/
/**
* The companies that developed the application
* @type {?Company[]}
*/
this.developers = data.developers;
} else {
this.developers ??= null;
}
if ('publishers' in data) {
/**
* The companies that published the application
* @type {?Company[]}
*/
this.publishers = data.publishers;
} else {
this.publishers ??= null;
}
if ('redirect_uris' in data) {
/**
* The whitelisted URLs for redirecting to during OAuth2 authorization (max 10)
* @type {?string[]}
*/
this.redirectUris = data.redirect_uris;
} else {
this.redirectUris ??= null;
}
if ('deeplink_uri' in data) {
/**
* The URL used for deep linking during OAuth2 authorization on mobile devices
* @type {?string}
*/
this.deeplinkUri = data.deeplink_uri;
} else {
this.deeplinkUri ??= null;
}
if ('integration_public' in data) {
/**
* Whether only the application owner can add the integration
* @type {?boolean}
*/
this.integrationPublic = data.integration_public;
} else {
this.integrationPublic ??= null;
}
if ('integration_require_code_grant' in data) {
/**
* Whether the integration will only be added upon completion of a full OAuth2 token exchange
* @type {?boolean}
*/
this.integrationRequireCodeGrant = data.integration_require_code_grant;
} else {
this.integrationRequireCodeGrant ??= null;
}
if ('bot_disabled' in data) {
/**
* Whether the application's bot is disabled by Discord (default false)
* @type {?boolean}
*/
this.botDisabled = data.bot_disabled;
} else {
this.botDisabled ??= null;
}
if ('bot_quarantined' in data) {
/**
* Whether the application's bot is quarantined by Discord
* @type {?boolean}
*/
this.botQuarantined = data.bot_quarantined;
} else {
this.botQuarantined ??= null;
}
if ('approximate_user_install_count' in data) {
/**
* Approximate count of users that have authorized the application with the applications.commands scope
* @type {?number}
*/
this.approximateUserInstallCount = data.approximate_user_install_count;
} else {
this.approximateUserInstallCount ??= null;
}
if ('approximate_user_authorization_count' in data) {
/**
* Approximate count of users that have OAuth2 authorizations for the application
* @type {?number}
*/
this.approximateUserAuthorizationCount = data.approximate_user_authorization_count;
} else {
this.approximateUserAuthorizationCount ??= null;
}
if ('internal_guild_restriction' in data) {
/**
* What guilds the application can be authorized in
* @type {?number}
*/
this.internalGuildRestriction = data.internal_guild_restriction;
} else {
this.internalGuildRestriction ??= null;
}
if ('interactions_endpoint_url' in data) {
/**
* The URL of the application's interactions endpoint
* @type {?string}
*/
this.interactionsEndpointUrl = data.interactions_endpoint_url;
} else {
this.interactionsEndpointUrl ??= null;
}
if ('interactions_version' in data) {
/**
* The version of the application's interactions endpoint implementation
* @type {?number}
*/
this.interactionsVersion = data.interactions_version;
} else {
this.interactionsVersion ??= null;
}
if ('interactions_event_types' in data) {
/**
* The enabled event webhook types to send to the interaction endpoint
* @type {?string[]}
*/
this.interactionsEventTypes = data.interactions_event_types;
} else {
this.interactionsEventTypes ??= null;
}
if ('event_webhooks_status' in data) {
/**
* Whether event webhooks are enabled
* @type {?number}
*/
this.eventWebhooksStatus = data.event_webhooks_status;
} else {
this.eventWebhooksStatus ??= null;
}
if ('event_webhooks_url' in data) {
/**
* The URL of the application's event webhooks endpoint
* @type {?string}
*/
this.eventWebhooksUrl = data.event_webhooks_url;
} else {
this.eventWebhooksUrl ??= null;
}
if ('event_webhooks_types' in data) {
/**
* The enabled event webhook types to send to the event webhooks endpoint
* @type {?string[]}
*/
this.eventWebhooksTypes = data.event_webhooks_types;
} else {
this.eventWebhooksTypes ??= null;
}
if ('explicit_content_filter' in data) {
/**
* Whether uploaded media content used in application commands is scanned and deleted for explicit content
* @type {?number}
*/
this.explicitContentFilter = data.explicit_content_filter;
} else {
this.explicitContentFilter ??= null;
}
if ('integration_types_config' in data) {
/**
* The configuration for each integration type supported by the application
* @type {?object}
*/
this.integrationTypesConfig = data.integration_types_config;
} else {
this.integrationTypesConfig ??= null;
}
if ('is_verified' in data) {
/**
* Whether the application is verified
* @type {?boolean}
*/
this.isVerified = data.is_verified;
} else {
this.isVerified ??= null;
}
if ('verification_state' in data) {
/**
* The current verification state of the application
* @type {?number}
*/
this.verificationState = data.verification_state;
} else {
this.verificationState ??= null;
}
if ('store_application_state' in data) {
/**
* The current store approval state of the commerce application
* @type {?number}
*/
this.storeApplicationState = data.store_application_state;
} else {
this.storeApplicationState ??= null;
}
if ('rpc_application_state' in data) {
/**
* The current RPC approval state of the application
* @type {?number}
*/
this.rpcApplicationState = data.rpc_application_state;
} else {
this.rpcApplicationState ??= null;
}
if ('creator_monetization_state' in data) {
/**
* The current guild creator monetization state of the application
* @type {?number}
*/
this.creatorMonetizationState = data.creator_monetization_state;
} else {
this.creatorMonetizationState ??= null;
}
if ('is_discoverable' in data) {
/**
* Whether the application is discoverable in the application directory
* @type {?boolean}
*/
this.isDiscoverable = data.is_discoverable;
} else {
this.isDiscoverable ??= null;
}
if ('discoverability_state' in data) {
/**
* The current application directory discoverability state of the application
* @type {?number}
*/
this.discoverabilityState = data.discoverability_state;
} else {
this.discoverabilityState ??= null;
}
if ('discovery_eligibility_flags' in data) {
/**
* The current application directory eligibility flags for the application
* @type {?number}
*/
this.discoveryEligibilityFlags = data.discovery_eligibility_flags;
} else {
this.discoveryEligibilityFlags ??= null;
}
if ('is_monetized' in data) {
/**
* Whether the application has monetization enabled
* @type {?boolean}
*/
this.isMonetized = data.is_monetized;
} else {
this.isMonetized ??= null;
}
if ('storefront_available' in data) {
/**
* Whether the application has public subscriptions or products available for purchase
* @type {?boolean}
*/
this.storefrontAvailable = data.storefront_available;
} else {
this.storefrontAvailable ??= null;
}
if ('monetization_state' in data) {
/**
* The current application monetization state of the application
* @type {?number}
*/
this.monetizationState = data.monetization_state;
} else {
this.monetizationState ??= null;
}
if ('monetization_eligibility_flags' in data) {
/**
* The current application monetization eligibility flags for the application
* @type {?number}
*/
this.monetizationEligibilityFlags = data.monetization_eligibility_flags;
} else {
this.monetizationEligibilityFlags ??= null;
}
if ('max_participants' in data) {
/**
* The maximum possible participants in the application's embedded activity (-1 for no limit)
* @type {?number}
*/
this.maxParticipants = data.max_participants;
} else {
this.maxParticipants ??= null;
}
} }
/** /**

20
typings/enums.d.ts vendored
View File

@@ -348,3 +348,23 @@ export const enum SeparatorSpacingSizes {
SMALL = 1, SMALL = 1,
LARGE = 2, LARGE = 2,
} }
export const enum ApplicationType {
/**
* A game integrating with Discord
*/
GAME = 1,
/**
* A music service integrating with Discord
* @deprecated
*/
MUSIC = 2,
/**
* A limited application used for ticketed event SKUs
*/
TICKETED_EVENTS = 3,
/**
* A limited application used for creator monetization (e.g. role subscription) SKUs
*/
CREATOR_MONETIZATION = 4,
}

65
typings/index.d.ts vendored
View File

@@ -120,6 +120,7 @@ import {
ReactionTypes, ReactionTypes,
MessageReferenceTypes, MessageReferenceTypes,
SeparatorSpacingSizes, SeparatorSpacingSizes,
ApplicationType,
} from './enums'; } from './enums';
import { import {
APIApplicationRoleConnectionMetadata, APIApplicationRoleConnectionMetadata,
@@ -350,7 +351,9 @@ export abstract class Application extends Base {
public verifyKey: string | null; public verifyKey: string | null;
public roleConnectionsVerificationURL: string | null; public roleConnectionsVerificationURL: string | null;
public approximateGuildCount: number | null; public approximateGuildCount: number | null;
/** @deprecated */
public botPublic: boolean | null; public botPublic: boolean | null;
/** @deprecated */
public botRequireCodeGrant: boolean | null; public botRequireCodeGrant: boolean | null;
public commands: ApplicationCommandManager; public commands: ApplicationCommandManager;
public cover: string | null; public cover: string | null;
@@ -363,6 +366,55 @@ export abstract class Application extends Base {
public owner: User | Team | null; public owner: User | Team | null;
public readonly partial: boolean; public readonly partial: boolean;
public rpcOrigins: string[]; public rpcOrigins: string[];
public splash: string | null;
public type: ApplicationType | null;
public primarySkuId: Snowflake | null;
public eulaId: Snowflake | null;
public slug: string | null;
public aliases: string[];
public executables: { os: string; name: string; is_launcher: boolean }[] | null;
public thirdPartySkus: { id: Snowflake | null; sku: string | null; distributor: string }[] | null;
public hook: boolean | null;
public overlay: boolean | null;
public overlayMethods: number | null;
public overlayWarn: boolean | null;
public overlayCompatibilityHook: boolean | null;
public bot: PartialUser | null;
public developers: { id: Snowflake; name: string }[] | null;
public publishers: { id: Snowflake; name: string }[] | null;
public redirectUris: string[] | null;
public deeplinkUri: string | null;
public integrationPublic: boolean | null;
public integrationRequireCodeGrant: boolean | null;
public botDisabled: boolean | null;
public botQuarantined: boolean | null;
public approximateUserInstallCount: number | null;
public approximateUserAuthorizationCount: number | null;
public internalGuildRestriction: number | null;
public interactionsEndpointUrl: string | null;
public interactionsVersion: number | null;
public interactionsEventTypes: string[] | null;
public eventWebhooksStatus: number | null;
public eventWebhooksUrl: string | null;
public eventWebhooksTypes: string[] | null;
public explicitContentFilter: number | null;
public integrationTypesConfig: Record<
number,
{ oauth2_install_params: ClientApplicationInstallParams | null }
> | null;
public isVerified: boolean | null;
public verificationState: number | null;
public storeApplicationState: number | null;
public rpcApplicationState: number | null;
public creatorMonetizationState: number | null;
public isDiscoverable: boolean | null;
public discoverabilityState: number | null;
public discoveryEligibilityFlags: number | null;
public isMonetized: boolean | null;
public storefrontAvailable: boolean | null;
public monetizationState: number | null;
public monetizationEligibilityFlags: number | null;
public maxParticipants: number | null;
public fetch(): Promise<Application>; public fetch(): Promise<Application>;
public fetchRoleConnectionMetadataRecords(): Promise<ApplicationRoleConnectionMetadata[]>; public fetchRoleConnectionMetadataRecords(): Promise<ApplicationRoleConnectionMetadata[]>;
public coverURL(options?: StaticImageURLOptions): string | null; public coverURL(options?: StaticImageURLOptions): string | null;
@@ -836,7 +888,18 @@ export class Client<Ready extends boolean = boolean> extends BaseClient {
public redeemNitro(nitro: string, channel?: TextChannelResolvable, paymentSourceId?: Snowflake): Promise<any>; public redeemNitro(nitro: string, channel?: TextChannelResolvable, paymentSourceId?: Snowflake): Promise<any>;
public authorizeURL(urlOAuth2: string, options?: OAuth2AuthorizeOptions): Promise<{ location: string }>; public authorizeURL(urlOAuth2: string, options?: OAuth2AuthorizeOptions): Promise<{ location: string }>;
public installUserApps(applicationId: Snowflake): Promise<void>; public installUserApps(applicationId: Snowflake): Promise<void>;
public deauthorize(applicationId: Snowflake): Promise<void>; public deauthorize(id: Snowflake, type?: 'application' | 'token'): Promise<void>;
public authorizedApplications(): Promise<
Collection<
Snowflake,
{
application: Application;
scopes: string[];
authorizedApplicationId: Snowflake;
deauthorize: () => Promise<void>;
}
>
>;
public on<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => Awaitable<void>): this; public on<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => Awaitable<void>): this;
public on<S extends string | symbol>( public on<S extends string | symbol>(