Skip to content

Commit

Permalink
feat: user-installable apps (#10227)
Browse files Browse the repository at this point in the history
* feat: inital user-installable apps support

* docs: add deprecation warnings

* feat: add equality checks

* fix: possibly `null` cases

* docs: tweaks

* docs: add deprecations

* fix(ApplicationCommandManager): amend transform command

* feat: properly support `integration_types_config`

* docs: add .

* docs: minor changes

* featBaseApplicationCommandData): update type

* style: prettier

* chore: fix issues

* fix: correct casing

Co-authored-by: Superchupu <[email protected]>

* refactor: remove console log

* fix: use case that satisfies `/core` and the API

* fix: `oauth2InstallParams` property is not nullable

* fix: do not convert keys into strings

* feat: update transforer to return the full map

* feat: update transformers

* feat: add `PartialGroupDMMessageManager `

Hope this is not a breaking change

* docs: fix type

* feat: add approximate count of users property

* fix: messageCreate doesn't emit in PartialGroupDMChannel

* fix: add GroupDM to TextBasedChannelTypes

* feat: add NonPartialGroupDMChannel helper

* fix: expect PartialGroupDMChannel

* feat: narrow generic type

* test: exclude PartialGroupDMChannel

* feat: use structure's channel type

* docs: narrow type

* feat: remove transformer

* refactor: remove unnecessary parse

* feat: add APIAutoModerationAction transformer

* fix: use the right transformer during recursive parsing of interaction metadata

* docs: add external types

* docs: add `Message#interactionMetadata` property docs

* docs: make nullable

* docs: add d-docs link

* docs: use optional

* fix: make `oauth2InstallParams` nullable

* types: update `IntegrationTypesConfiguration`

Co-authored-by: Almeida <[email protected]>

* docs: update `IntegrationTypesConfigurationParameters`

Co-authored-by: Almeida <[email protected]>

* types: update `IntegrationTypesConfigurationParameters`

* refactor: improve readability

* docs: mark integrationTypesConfig nullable

* refactor: requested changes

---------

Co-authored-by: Jiralite <[email protected]>
Co-authored-by: Superchupu <[email protected]>
Co-authored-by: Vlad Frangu <[email protected]>
Co-authored-by: Almeida <[email protected]>
  • Loading branch information
5 people authored Sep 1, 2024
1 parent a5afc40 commit fc0b6f7
Show file tree
Hide file tree
Showing 15 changed files with 278 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export class ContextMenuCommandBuilder {
*
* @remarks
* By default, commands are visible. This property is only for global commands.
* @deprecated
* Use {@link ContextMenuCommandBuilder.contexts} instead.
*/
public readonly dm_permission: boolean | undefined = undefined;

Expand Down Expand Up @@ -167,6 +169,7 @@ export class ContextMenuCommandBuilder {
* By default, commands are visible. This method is only for global commands.
* @param enabled - Whether the command should be enabled in direct messages
* @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions}
* @deprecated Use {@link ContextMenuCommandBuilder.setContexts} instead.
*/
public setDMPermission(enabled: boolean | null | undefined) {
// Assert the value matches the conditions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export class SlashCommandBuilder {
*
* @remarks
* By default, commands are visible. This property is only for global commands.
* @deprecated
* Use {@link SlashCommandBuilder.contexts} instead.
*/
public readonly dm_permission: boolean | undefined = undefined;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export class SharedSlashCommand {

public readonly default_member_permissions: Permissions | null | undefined = undefined;

/**
* @deprecated Use {@link SharedSlashCommand.contexts} instead.
*/
public readonly dm_permission: boolean | undefined = undefined;

public readonly integration_types?: ApplicationIntegrationType[];
Expand Down Expand Up @@ -113,6 +116,8 @@ export class SharedSlashCommand {
* By default, commands are visible. This method is only for global commands.
* @param enabled - Whether the command should be enabled in direct messages
* @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions}
* @deprecated
* Use {@link SharedSlashCommand.setContexts} instead.
*/
public setDMPermission(enabled: boolean | null | undefined) {
// Assert the value matches the conditions
Expand Down
2 changes: 2 additions & 0 deletions packages/discord.js/src/managers/ApplicationCommandManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@ class ApplicationCommandManager extends CachedManager {
options: command.options?.map(option => ApplicationCommand.transformOption(option)),
default_member_permissions,
dm_permission: command.dmPermission ?? command.dm_permission,
integration_types: command.integrationTypes ?? command.integration_types,
contexts: command.contexts,
};
}
}
Expand Down
17 changes: 17 additions & 0 deletions packages/discord.js/src/managers/PartialGroupDMMessageManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict';

const MessageManager = require('./MessageManager');

/**
* Manages API methods for messages in group direct message channels and holds their cache.
* @extends {MessageManager}
*/
class PartialGroupDMMessageManager extends MessageManager {
/**
* The channel that the messages belong to
* @name PartialGroupDMMessageManager#channel
* @type {PartialGroupDMChannel}
*/
}

module.exports = PartialGroupDMMessageManager;
27 changes: 26 additions & 1 deletion packages/discord.js/src/structures/ApplicationCommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,35 @@ class ApplicationCommand extends Base {
* Whether the command can be used in DMs
* <info>This property is always `null` on guild commands</info>
* @type {?boolean}
* @deprecated Use {@link ApplicationCommand#contexts} instead.
*/
this.dmPermission = data.dm_permission;
} else {
this.dmPermission ??= null;
}

if ('integration_types' in data) {
/**
* Installation context(s) where the command is available
* <info>Only for globally-scoped commands</info>
* @type {?ApplicationIntegrationType[]}
*/
this.integrationTypes = data.integration_types;
} else {
this.integrationTypes ??= null;
}

if ('contexts' in data) {
/**
* Interaction context(s) where the command can be used
* <info>Only for globally-scoped commands</info>
* @type {?InteractionContextType[]}
*/
this.contexts = data.contexts;
} else {
this.contexts ??= null;
}

if ('version' in data) {
/**
* Autoincrementing version identifier updated during substantial record changes
Expand Down Expand Up @@ -394,7 +417,9 @@ class ApplicationCommand extends Base {
!isEqual(
command.descriptionLocalizations ?? command.description_localizations ?? {},
this.descriptionLocalizations ?? {},
)
) ||
!isEqual(command.integrationTypes ?? command.integration_types ?? [], this.integrationTypes ?? {}) ||
!isEqual(command.contexts ?? [], this.contexts ?? [])
) {
return false;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/discord.js/src/structures/BaseInteraction.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ class BaseInteraction extends Base {

/**
* Set of permissions the application or bot has within the channel the interaction was sent from
* @type {?Readonly<PermissionsBitField>}
* @type {Readonly<PermissionsBitField>}
*/
this.appPermissions = data.app_permissions ? new PermissionsBitField(data.app_permissions).freeze() : null;
this.appPermissions = new PermissionsBitField(data.app_permissions).freeze();

/**
* The permissions of the member, if one exists, in the channel this interaction was executed in
Expand Down
64 changes: 62 additions & 2 deletions packages/discord.js/src/structures/ClientApplication.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ const PermissionsBitField = require('../util/PermissionsBitField');

/**
* @typedef {Object} ClientApplicationInstallParams
* @property {OAuth2Scopes[]} scopes The scopes to add the application to the server with
* @property {Readonly<PermissionsBitField>} permissions The permissions this bot will request upon joining
* @property {OAuth2Scopes[]} scopes Scopes that will be set upon adding this application
* @property {Readonly<PermissionsBitField>} permissions Permissions that will be requested for the integrated role
*/

/**
Expand Down Expand Up @@ -68,6 +68,56 @@ class ClientApplication extends Application {
this.installParams ??= null;
}

/**
* OAuth2 installation parameters.
* @typedef {Object} IntegrationTypesConfigurationParameters
* @property {OAuth2Scopes[]} scopes Scopes that will be set upon adding this application
* @property {Readonly<PermissionsBitField>} permissions Permissions that will be requested for the integrated role
*/

/**
* The application's supported installation context data.
* @typedef {Object} IntegrationTypesConfigurationContext
* @property {?IntegrationTypesConfigurationParameters} oauth2InstallParams
* Scopes and permissions regarding the installation context
*/

/**
* The application's supported installation context data.
* @typedef {Object} IntegrationTypesConfiguration
* @property {IntegrationTypesConfigurationContext} [0] Scopes and permissions
* regarding the guild-installation context
* @property {IntegrationTypesConfigurationContext} [1] Scopes and permissions
* regarding the user-installation context
*/

if ('integration_types_config' in data) {
/**
* Default scopes and permissions for each supported installation context.
* The keys are stringified variants of {@link ApplicationIntegrationType}.
* @type {?IntegrationTypesConfiguration}
*/
this.integrationTypesConfig = Object.fromEntries(
Object.entries(data.integration_types_config).map(([key, config]) => {
let oauth2InstallParams = null;
if (config.oauth2_install_params) {
oauth2InstallParams = {
scopes: config.oauth2_install_params.scopes,
permissions: new PermissionsBitField(config.oauth2_install_params.permissions).freeze(),
};
}

const context = {
oauth2InstallParams,
};

return [key, context];
}),
);
} else {
this.integrationTypesConfig ??= null;
}

if ('custom_install_url' in data) {
/**
* This application's custom installation URL
Expand Down Expand Up @@ -96,6 +146,16 @@ class ClientApplication extends Application {
this.approximateGuildCount ??= null;
}

if ('approximate_user_install_count' in data) {
/**
* An approximate amount of users that have installed this application.
* @type {?number}
*/
this.approximateUserInstallCount = data.approximate_user_install_count;
} else {
this.approximateUserInstallCount ??= null;
}

if ('guild_id' in data) {
/**
* The id of the guild associated with this application.
Expand Down
15 changes: 15 additions & 0 deletions packages/discord.js/src/structures/CommandInteraction.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,21 @@ class CommandInteraction extends BaseInteraction {
*/
this.commandGuildId = data.data.guild_id ?? null;

/* eslint-disable max-len */
/**
* Mapping of installation contexts that the interaction was authorized for the related user or guild ids
* @type {APIAuthorizingIntegrationOwnersMap}
* @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-authorizing-integration-owners-object}
*/
this.authorizingIntegrationOwners = data.authorizing_integration_owners;
/* eslint-enable max-len */

/**
* Context where the interaction was triggered from
* @type {?InteractionContextType}
*/
this.context = data.context ?? null;

/**
* Whether the reply to this interaction has been deferred
* @type {boolean}
Expand Down
29 changes: 29 additions & 0 deletions packages/discord.js/src/structures/Message.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const { createComponent } = require('../util/Components');
const { NonSystemMessageTypes, MaxBulkDeletableMessageAge, UndeletableMessageTypes } = require('../util/Constants');
const MessageFlagsBitField = require('../util/MessageFlagsBitField');
const PermissionsBitField = require('../util/PermissionsBitField');
const { _transformAPIMessageInteractionMetadata } = require('../util/Transformers.js');
const { cleanContent, resolvePartialEmoji, transformResolved } = require('../util/Util');

/**
Expand Down Expand Up @@ -383,6 +384,33 @@ class Message extends Base {
this.channel?.messages._add({ guild_id: data.message_reference?.guild_id, ...data.referenced_message });
}

if (data.interaction_metadata) {
/**
* Partial data of the interaction that a message is a result of
* @typedef {Object} MessageInteractionMetadata
* @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 {?Snowflake} originalResponseMessageId
* Id of the original response message. Present only on follow-up messages
* @property {?Snowflake} interactedMessageId
* Id of the message that contained interactive component.
* Present only on messages created from component interactions
* @property {?MessageInteractionMetadata} triggeringInteractionMetadata
* Metadata for the interaction that was used to open the modal. Present only on modal submit interactions
*/

/**
* Partial data of the interaction that this message is a result of
* @type {?MessageInteractionMetadata}
*/
this.interactionMetadata = _transformAPIMessageInteractionMetadata(this.client, data.interaction_metadata);
} else {
this.interactionMetadata ??= null;
}

/**
* Partial data of the interaction that a message is a reply to
* @typedef {Object} MessageInteraction
Expand All @@ -391,6 +419,7 @@ class Message extends Base {
* @property {string} commandName The name of the interaction's application command,
* as well as the subcommand and subcommand group, where applicable
* @property {User} user The user that invoked the interaction
* @deprecated Use {@link Message#interactionMetadata} instead.
*/

if (data.interaction) {
Expand Down
7 changes: 7 additions & 0 deletions packages/discord.js/src/structures/PartialGroupDMChannel.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const { BaseChannel } = require('./BaseChannel');
const { DiscordjsError, ErrorCodes } = require('../errors');
const PartialGroupDMMessageManager = require('../managers/PartialGroupDMMessageManager');

/**
* Represents a Partial Group DM Channel on Discord.
Expand Down Expand Up @@ -37,6 +38,12 @@ class PartialGroupDMChannel extends BaseChannel {
* @type {PartialRecipient[]}
*/
this.recipients = data.recipients;

/**
* A manager of the messages belonging to this channel
* @type {PartialGroupDMMessageManager}
*/
this.messages = new PartialGroupDMMessageManager(this);
}

/**
Expand Down
20 changes: 20 additions & 0 deletions packages/discord.js/src/util/APITypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIApplicationCommandOption}
*/

/**
* @external ApplicationIntegrationType
* @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}
Expand Down Expand Up @@ -140,6 +150,11 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIMessageComponentEmoji}
*/

/**
* @external APIMessageInteractionMetadata
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIMessageInteractionMetadata}
*/

/**
* @external APIModalInteractionResponse
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIModalInteractionResponse}
Expand Down Expand Up @@ -400,6 +415,11 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/IntegrationExpireBehavior}
*/

/**
* @external InteractionContextType
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/InteractionContextType}
*/

/**
* @external InteractionType
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/InteractionType}
Expand Down
23 changes: 22 additions & 1 deletion packages/discord.js/src/util/Transformers.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,25 @@ function _transformAPIAutoModerationAction(autoModerationAction) {
};
}

module.exports = { toSnakeCase, _transformAPIAutoModerationAction };
/**
* Transforms an API message interaction metadata object to a camel-cased variant.
* @param {Client} client The client
* @param {APIMessageInteractionMetadata} messageInteractionMetadata The metadata to transform
* @returns {MessageInteractionMetadata}
* @ignore
*/
function _transformAPIMessageInteractionMetadata(client, messageInteractionMetadata) {
return {
id: messageInteractionMetadata.id,
type: messageInteractionMetadata.type,
user: client.users._add(messageInteractionMetadata.user),
authorizingIntegrationOwners: messageInteractionMetadata.authorizing_integration_owners,
originalResponseMessageId: messageInteractionMetadata.original_response_message_id ?? null,
interactedMessageId: messageInteractionMetadata.interacted_message_id ?? null,
triggeringInteractionMetadata: messageInteractionMetadata.triggering_interaction_metadata
? _transformAPIMessageInteractionMetadata(messageInteractionMetadata.triggering_interaction_metadata)
: null,
};
}

module.exports = { toSnakeCase, _transformAPIAutoModerationAction, _transformAPIMessageInteractionMetadata };
Loading

0 comments on commit fc0b6f7

Please sign in to comment.