From b6db455b9963c68864d2f505b728f6d4bac7f888 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Wed, 17 Dec 2025 20:55:05 +0200 Subject: [PATCH] feat: proper authorizing integration owners structure (#11366) * feat: proper authorizing integration owners structure chore: fix ci chore: fix types chore: fix types chore: nits chore: tests chore: requested changes chore: drop it from apitypes chore: requested 2 chore: rofl chore: docs * chore: docs * chore: docs * Apply suggestions from code review Co-authored-by: Almeida Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com> --------- Co-authored-by: Almeida Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- packages/discord.js/src/index.js | 1 + .../AuthorizingIntegrationOwners.js | 66 +++++++++++++++++++ .../src/structures/BaseInteraction.js | 11 +++- packages/discord.js/src/structures/Message.js | 4 +- packages/discord.js/src/util/APITypes.js | 5 -- packages/discord.js/src/util/Transformers.js | 6 +- packages/discord.js/typings/index.d.ts | 20 +++++- packages/discord.js/typings/index.test-d.ts | 11 ++++ 8 files changed, 111 insertions(+), 13 deletions(-) create mode 100644 packages/discord.js/src/structures/AuthorizingIntegrationOwners.js diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js index d823f28d9..cff48a030 100644 --- a/packages/discord.js/src/index.js +++ b/packages/discord.js/src/index.js @@ -111,6 +111,7 @@ exports.AutocompleteInteraction = require('./structures/AutocompleteInteraction' exports.AutoModerationActionExecution = require('./structures/AutoModerationActionExecution'); exports.AutoModerationRule = require('./structures/AutoModerationRule'); exports.Base = require('./structures/Base'); +exports.AuthorizingIntegrationOwners = require('./structures/AuthorizingIntegrationOwners'); exports.BaseGuild = require('./structures/BaseGuild'); exports.BaseGuildEmoji = require('./structures/BaseGuildEmoji'); exports.BaseGuildTextChannel = require('./structures/BaseGuildTextChannel'); diff --git a/packages/discord.js/src/structures/AuthorizingIntegrationOwners.js b/packages/discord.js/src/structures/AuthorizingIntegrationOwners.js new file mode 100644 index 000000000..fbd81427e --- /dev/null +++ b/packages/discord.js/src/structures/AuthorizingIntegrationOwners.js @@ -0,0 +1,66 @@ +'use strict'; + +const { ApplicationIntegrationType } = require('discord-api-types/v10'); +const Base = require('./Base'); + +/** + * Represents the owners of an authorizing integration. + * + * @extends {Base} + */ +class AuthorizingIntegrationOwners extends Base { + constructor(client, data) { + super(client); + + Object.defineProperty(this, 'data', { value: data }); + + // Support accessing values by integration type, such as + // authorizingIntegrationOwners[ApplicationIntegrationType.GuildInstall], + // and preserve forward compatibility if new installation types are added. + for (const value of Object.values(ApplicationIntegrationType)) { + if (typeof value !== 'number') { + continue; + } + + Object.defineProperty(this, value, { value: this.data[value] }); + } + + /** + * The id of the guild where the integration is installed, if applicable. + * + * @type {?Snowflake} + */ + this.guildId = this.data[ApplicationIntegrationType.GuildInstall] ?? null; + + /** + * The id of the user on which the integration is installed, if applicable. + * + * @type {?Snowflake} + */ + this.userId = this.data[ApplicationIntegrationType.UserInstall] ?? null; + } + + /** + * The guild where the integration is installed, if applicable. + * + * @type {?Guild} + */ + get guild() { + return (this.guildId && this.client.guilds.cache.get(this.guildId)) ?? null; + } + + /** + * The user on which the integration is installed, if applicable. + * + * @type {?User} + */ + get user() { + return (this.userId && this.client.users.cache.get(this.userId)) ?? null; + } + + toJSON() { + return this.data; + } +} + +module.exports = AuthorizingIntegrationOwners; diff --git a/packages/discord.js/src/structures/BaseInteraction.js b/packages/discord.js/src/structures/BaseInteraction.js index 68817b331..e588d9233 100644 --- a/packages/discord.js/src/structures/BaseInteraction.js +++ b/packages/discord.js/src/structures/BaseInteraction.js @@ -4,6 +4,7 @@ const { deprecate } = require('node:util'); const { Collection } = require('@discordjs/collection'); const { DiscordSnowflake } = require('@sapphire/snowflake'); const { InteractionType, ApplicationCommandType, ComponentType } = require('discord-api-types/v10'); +const AuthorizingIntegrationOwners = require('./AuthorizingIntegrationOwners'); const Base = require('./Base'); const { SelectMenuTypes } = require('../util/Constants'); const PermissionsBitField = require('../util/PermissionsBitField'); @@ -110,11 +111,15 @@ class BaseInteraction extends Base { /* eslint-disable max-len */ /** - * Mapping of installation contexts that the interaction was authorized for the related user or guild ids - * @type {APIAuthorizingIntegrationOwnersMap} + * Mapping of integration types that the application was authorized for the related user or guild ids + * + * @type {AuthorizingIntegrationOwners} * @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-authorizing-integration-owners-object} */ - this.authorizingIntegrationOwners = data.authorizing_integration_owners; + this.authorizingIntegrationOwners = new AuthorizingIntegrationOwners( + this.client, + data.authorizing_integration_owners, + ); /* eslint-enable max-len */ /** diff --git a/packages/discord.js/src/structures/Message.js b/packages/discord.js/src/structures/Message.js index 4b5c8424b..e7152b77a 100644 --- a/packages/discord.js/src/structures/Message.js +++ b/packages/discord.js/src/structures/Message.js @@ -394,8 +394,8 @@ class Message extends Base { * @property {Snowflake} id The interaction's id * @property {InteractionType} type The type of the interaction * @property {User} user The user that invoked the interaction - * @property {APIAuthorizingIntegrationOwnersMap} authorizingIntegrationOwners - * Ids for installation context(s) related to an interaction + * @property {AuthorizingIntegrationOwners} authorizingIntegrationOwners + * Mapping of integration types that the application was authorized for the related user or guild ids * @property {?Snowflake} originalResponseMessageId * Id of the original response message. Present only on follow-up messages * @property {?Snowflake} interactedMessageId diff --git a/packages/discord.js/src/util/APITypes.js b/packages/discord.js/src/util/APITypes.js index 8fd25eff0..dca194dba 100644 --- a/packages/discord.js/src/util/APITypes.js +++ b/packages/discord.js/src/util/APITypes.js @@ -40,11 +40,6 @@ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ApplicationIntegrationType} */ -/** - * @external APIAuthorizingIntegrationOwnersMap - * @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIAuthorizingIntegrationOwnersMap} - */ - /** * @external APIAutoModerationAction * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIAutoModerationAction} diff --git a/packages/discord.js/src/util/Transformers.js b/packages/discord.js/src/util/Transformers.js index d875ae19d..195833753 100644 --- a/packages/discord.js/src/util/Transformers.js +++ b/packages/discord.js/src/util/Transformers.js @@ -3,6 +3,7 @@ const { isJSONEncodable } = require('@discordjs/util'); const snakeCase = require('lodash.snakecase'); const { resolvePartialEmoji } = require('./Util'); +const AuthorizingIntegrationOwners = require('../structures/AuthorizingIntegrationOwners'); /** * Transforms camel-cased keys into snake cased keys @@ -53,7 +54,10 @@ function _transformAPIMessageInteractionMetadata(client, messageInteractionMetad id: messageInteractionMetadata.id, type: messageInteractionMetadata.type, user: client.users._add(messageInteractionMetadata.user), - authorizingIntegrationOwners: messageInteractionMetadata.authorizing_integration_owners, + authorizingIntegrationOwners: new AuthorizingIntegrationOwners( + client, + messageInteractionMetadata.authorizing_integration_owners, + ), originalResponseMessageId: messageInteractionMetadata.original_response_message_id ?? null, interactedMessageId: messageInteractionMetadata.interacted_message_id ?? null, triggeringInteractionMetadata: messageInteractionMetadata.triggering_interaction_metadata diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index b052e255f..035eff90e 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -2175,7 +2175,7 @@ export class BaseInteraction extends Base private readonly _cacheType: Cached; protected constructor(client: Client, data: RawInteractionData); public applicationId: Snowflake; - public authorizingIntegrationOwners: APIAuthorizingIntegrationOwnersMap; + public authorizingIntegrationOwners: AuthorizingIntegrationOwners; public get channel(): CacheTypeReducer< Cached, GuildTextBasedChannel | null, @@ -2543,6 +2543,22 @@ export class Message extends Base { public inGuild(): this is Message; } +export class AuthorizingIntegrationOwners extends Base { + private constructor(client: Client, data: APIAuthorizingIntegrationOwnersMap); + private readonly data: APIAuthorizingIntegrationOwnersMap; + + // Getters from types + public readonly [ApplicationIntegrationType.GuildInstall]?: Snowflake; + public readonly [ApplicationIntegrationType.UserInstall]?: Snowflake; + + public readonly guildId: Snowflake | null; + public get guild(): Guild | null; + public readonly userId: Snowflake | null; + public get user(): User | null; + + public toJSON(): APIAuthorizingIntegrationOwnersMap; +} + export class AttachmentBuilder { public constructor(attachment: BufferResolvable | Stream, data?: AttachmentData); public attachment: BufferResolvable | Stream; @@ -7353,7 +7369,7 @@ export interface MessageInteractionMetadata { id: Snowflake; type: InteractionType; user: User; - authorizingIntegrationOwners: APIAuthorizingIntegrationOwnersMap; + authorizingIntegrationOwners: AuthorizingIntegrationOwners; originalResponseMessageId: Snowflake | null; interactedMessageId: Snowflake | null; triggeringInteractionMetadata: MessageInteractionMetadata | null; diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index ea97c3ec3..4a6722464 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -12,6 +12,7 @@ import { ApplicationCommandOptionType, ComponentType, ApplicationCommandPermissionType, + ApplicationIntegrationType, ChannelType, InteractionType, GatewayIntentBits, @@ -203,6 +204,7 @@ import { SKU, UserSelectMenuBuilder, VoiceServerUpdateData, + AuthorizingIntegrationOwners, RoleSelectMenuBuilder, ChannelSelectMenuBuilder, MentionableSelectMenuBuilder, @@ -3071,3 +3073,12 @@ await guildScheduledEventManager.edit(snowflake, { recurrenceRule: null }); byMonth: [GuildScheduledEventRecurrenceRuleMonth.May], }); } + +declare const authorizingIntegrationOwners: AuthorizingIntegrationOwners; +{ + expectType(authorizingIntegrationOwners.guildId); + expectType(authorizingIntegrationOwners.guild); + expectType(authorizingIntegrationOwners.userId); + expectType(authorizingIntegrationOwners.user); + expectType(authorizingIntegrationOwners[ApplicationIntegrationType.GuildInstall]); +}